Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
{
|
|
|
|
|
|
"username": "使用者名稱",
|
|
|
|
|
|
"password": "密碼",
|
|
|
|
|
|
"login": "登入",
|
|
|
|
|
|
"confirm": "確定",
|
|
|
|
|
|
"cancel": "取消",
|
|
|
|
|
|
"close": "關閉",
|
|
|
|
|
|
"save": "儲存",
|
|
|
|
|
|
"logout": "登出",
|
|
|
|
|
|
"create": "建立",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"add": "新增",
|
|
|
|
|
|
"remove": "移除",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"update": "更新",
|
|
|
|
|
|
"copy": "複製",
|
|
|
|
|
|
"copied": "已複製",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"more": "更多",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"download": "下載",
|
|
|
|
|
|
"remark": "備註",
|
|
|
|
|
|
"enable": "啟用",
|
|
|
|
|
|
"protocol": "協議",
|
|
|
|
|
|
"search": "搜尋",
|
|
|
|
|
|
"filter": "篩選",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"all": "全部",
|
|
|
|
|
|
"from": "從",
|
|
|
|
|
|
"to": "到",
|
|
|
|
|
|
"done": "完成",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"loading": "載入中...",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"refresh": "重新整理",
|
|
|
|
|
|
"clear": "清除",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"second": "秒",
|
|
|
|
|
|
"minute": "分鐘",
|
|
|
|
|
|
"hour": "小時",
|
|
|
|
|
|
"day": "天",
|
|
|
|
|
|
"check": "檢視",
|
|
|
|
|
|
"indefinite": "無限期",
|
|
|
|
|
|
"unlimited": "無限制",
|
|
|
|
|
|
"none": "無",
|
|
|
|
|
|
"qrCode": "二維碼",
|
|
|
|
|
|
"info": "更多資訊",
|
|
|
|
|
|
"edit": "編輯",
|
|
|
|
|
|
"delete": "刪除",
|
|
|
|
|
|
"reset": "重置",
|
|
|
|
|
|
"noData": "無數據。",
|
|
|
|
|
|
"copySuccess": "複製成功",
|
|
|
|
|
|
"sure": "確定",
|
|
|
|
|
|
"encryption": "加密",
|
|
|
|
|
|
"useIPv4ForHost": "使用 IPv4 連接主機",
|
|
|
|
|
|
"transmission": "傳輸",
|
|
|
|
|
|
"host": "主機",
|
|
|
|
|
|
"path": "路徑",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"camouflage": "混淆",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"status": "狀態",
|
|
|
|
|
|
"enabled": "開啟",
|
|
|
|
|
|
"disabled": "關閉",
|
|
|
|
|
|
"depleted": "耗盡",
|
|
|
|
|
|
"depletingSoon": "即將耗盡",
|
|
|
|
|
|
"offline": "離線",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"online": "上線",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"domainName": "域名",
|
|
|
|
|
|
"monitor": "監聽",
|
|
|
|
|
|
"certificate": "憑證",
|
|
|
|
|
|
"fail": "失敗",
|
|
|
|
|
|
"comment": "評論",
|
|
|
|
|
|
"success": "成功",
|
|
|
|
|
|
"lastOnline": "上次上線",
|
|
|
|
|
|
"getVersion": "獲取版本",
|
|
|
|
|
|
"install": "安裝",
|
|
|
|
|
|
"clients": "客戶端",
|
|
|
|
|
|
"usage": "使用情況",
|
|
|
|
|
|
"twoFactorCode": "代碼",
|
|
|
|
|
|
"remained": "剩餘",
|
|
|
|
|
|
"security": "安全",
|
|
|
|
|
|
"secAlertTitle": "安全警報",
|
|
|
|
|
|
"secAlertSsl": "此連線不安全。在啟用 TLS 進行資料保護之前,請勿輸入敏感資訊。",
|
|
|
|
|
|
"secAlertConf": "某些設定易受攻擊。建議加強安全協議以防止潛在漏洞。",
|
|
|
|
|
|
"secAlertSSL": "面板缺少安全連線。請安裝 TLS 證書以保護資料安全。",
|
|
|
|
|
|
"secAlertPanelPort": "面板預設埠存在安全風險。請配置隨機埠或特定埠。",
|
|
|
|
|
|
"secAlertPanelURI": "面板預設 URI 路徑不安全。請配置複雜的 URI 路徑。",
|
|
|
|
|
|
"secAlertSubURI": "訂閱預設 URI 路徑不安全。請配置複雜的 URI 路徑。",
|
|
|
|
|
|
"secAlertSubJsonURI": "訂閱 JSON 預設 URI 路徑不安全。請配置複雜的 URI 路徑。",
|
|
|
|
|
|
"emptyDnsDesc": "未添加DNS伺服器。",
|
|
|
|
|
|
"emptyFakeDnsDesc": "未添加Fake DNS伺服器。",
|
|
|
|
|
|
"emptyBalancersDesc": "未添加負載平衡器。",
|
|
|
|
|
|
"emptyReverseDesc": "未添加反向代理。",
|
|
|
|
|
|
"somethingWentWrong": "發生錯誤",
|
|
|
|
|
|
"subscription": {
|
|
|
|
|
|
"title": "訂閱資訊",
|
|
|
|
|
|
"subId": "訂閱 ID",
|
|
|
|
|
|
"status": "狀態",
|
|
|
|
|
|
"downloaded": "已下載",
|
|
|
|
|
|
"uploaded": "已上傳",
|
|
|
|
|
|
"expiry": "到期",
|
|
|
|
|
|
"totalQuota": "總配額",
|
|
|
|
|
|
"individualLinks": "個別連結",
|
|
|
|
|
|
"active": "啟用",
|
|
|
|
|
|
"inactive": "停用",
|
|
|
|
|
|
"unlimited": "無限制",
|
|
|
|
|
|
"noExpiry": "無到期"
|
|
|
|
|
|
},
|
|
|
|
|
|
"menu": {
|
|
|
|
|
|
"theme": "主題",
|
|
|
|
|
|
"dark": "深色",
|
|
|
|
|
|
"ultraDark": "超深色",
|
|
|
|
|
|
"dashboard": "系統狀態",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"inbounds": "入站",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"clients": "客戶端",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"groups": "群組",
|
2026-05-10 09:55:37 +00:00
|
|
|
|
"nodes": "節點",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"settings": "面板設定",
|
|
|
|
|
|
"xray": "Xray 設定",
|
2026-05-11 18:47:49 +00:00
|
|
|
|
"apiDocs": "API 文件",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"logout": "退出登入",
|
Frontend rewrite: React + TypeScript with AntD v6 (#4498)
* chore(frontend): add react+typescript toolchain alongside vue
Step 0 of the planned vue->react migration. React 19, antd 5, i18next
+ react-i18next, typescript 5, and @vitejs/plugin-react 6 are added as
dev/runtime deps alongside the existing vue stack. Both frameworks
coexist in the build until the last entry flips.
* vite.config.js: react() plugin runs next to vue(); new manualChunks
for vendor-react / vendor-antd-react / vendor-icons-react /
vendor-i18next. Existing vue chunks unchanged.
* eslint.config.js: typescript-eslint + eslint-plugin-react-hooks
rules scoped to *.{ts,tsx}; vue config untouched for *.{js,vue}.
* tsconfig.json: strict, jsx: react-jsx, moduleResolution: bundler,
allowJs: true (lets .tsx files import the remaining .js modules
during incremental migration), @/* path alias.
* env.d.ts: Vite client types + window.X_UI_BASE_PATH typing +
SubPageData shape consumed by the subscription page.
Vite stays pinned at 8.0.13 per the existing project policy. No
existing .vue/.js source files touched in this step.
eslint-plugin-react (not -hooks) is not included because its latest
release does not yet support ESLint 10. react-hooks/purity covers
the safety-critical case; revisit when the plugin updates.
* refactor(frontend): port subpage to react+ts
Step 1 of the planned vue->react migration. The standalone
subscription page (sub/sub.go renders the HTML host; React mounts
into #app) is the first entry off vue.
Introduces two shared pieces both entries (and future ones) will
use:
* src/hooks/useTheme.tsx — React Context + useTheme hook + the
same buildAntdThemeConfig (dark/ultra-dark token overrides) and
pauseAnimationsUntilLeave helper the vue version exposes. Same
localStorage keys (dark-mode, isUltraDarkThemeEnabled) and DOM
side effects (body.className, html[data-theme]) so the two stay
in sync across the coexistence period.
* src/i18n/react.ts — i18next + react-i18next loader that reads
the same web/translation/*.json files via import.meta.glob. The
vue-i18n setup in src/i18n/index.js is untouched and still serves
the remaining vue entries.
SubPage.tsx mirrors the vue version's behavior: reads
window.__SUB_PAGE_DATA__ injected by the Go sub server, renders QR
codes / descriptions / Android+iOS deep-link dropdowns, supports
theme cycle and language switch. Uses AntD v5 idioms: Descriptions
items prop, Dropdown menu prop, Layout.Content.
* refactor(frontend): port login to react+ts
Step 2 of the planned vue->react migration. The login entry is the
first to exercise AntD React's Form API (Form + Form.Item with
name/rules + onFinish) and the existing axios/CSRF interceptors
under React.
* LoginPage.tsx: same form fields, conditional 2FA input,
rotating headline ("Hello" / "Welcome to..."), drifting blob
background, theme cycle + language popover. Headline transition
switches from vue's <Transition mode=out-in> to a CSS keyframe
animation keyed off the visible word.
* entries/login.tsx: setupAxios() + applyDocumentTitle() unchanged
from the vue entry — both are framework-agnostic in src/utils
and src/api/axios-init.js.
useTheme hook, ThemeProvider, and i18n/react.ts loader introduced
in step 1 are now shared across two entries; Vite extracts them as
a small chunk in the build output.
* refactor(frontend): port api-docs to react+ts
Step 3 of the planned vue->react migration. The five api-docs files
(ApiDocsPage, CodeBlock, EndpointRow, EndpointSection, plus the
data-only endpoints.js) all move to react+ts.
Also introduces components/AppSidebar.tsx — api-docs is the first
authenticated page to need it. AppSidebar.vue stays in place for the
six remaining vue entries (settings, inbounds, clients, xray, nodes,
index); each gets switched to AppSidebar.tsx as its entry migrates.
After the last entry flips, AppSidebar.vue is deleted.
Notable transformations:
* The scroll observer that highlights the active TOC link is a
useEffect keyed on sections — re-registers whenever the visible
set changes (search filter narrows it). Same behaviour as the vue
watchEffect.
* v-html="safeInlineHtml(...)" becomes
dangerouslySetInnerHTML={{ __html: safeInlineHtml(...) }}. The
helper still escapes everything except <code> tags.
* JSON syntax highlighter in CodeBlock is unchanged — pure regex on
the escaped string, then rendered via dangerouslySetInnerHTML.
* endpoints.js stays as JS (allowJs in tsconfig); only the consumer
signatures (Endpoint, Section) are typed at the React boundary.
* AppSidebar reuses pauseAnimationsUntilLeave + useTheme from
step 1. Drawer + Sider keyed off the same localStorage flag
(isSidebarCollapsed) and DOM theme attributes the vue version
uses, so the two stay in sync during coexistence.
* refactor(frontend): port nodes to react+ts
Step 4 of the planned vue->react migration. The nodes entry brings in
the largest shared-infrastructure batch so far — every authenticated
react page from here on can lean on these.
New shared pieces (live alongside their .vue counterparts during
coexistence):
* hooks/useMediaQuery.ts — useState + resize listener
* hooks/useWebSocket.ts — wraps WebSocketClient, subscribes on mount
and unsubscribes on unmount. The underlying client is a single
module-level instance so multiple components on the same page
share one socket.
* hooks/useNodes.ts — node list state + CRUD + probe/test, including
the totals memo (online/offline/avgLatency) used by the summary card.
applyNodesEvent is the entry point for the heartbeat-pushed list.
* components/CustomStatistic.tsx — thin Statistic wrapper, prefix +
suffix slots become props.
* components/Sparkline.tsx — the SVG line chart with measured-width
axis scaling, gradient fill, tooltip overlay, and per-instance
gradient id from React.useId. ResizeObserver lifecycle is in
useEffect; the math is unchanged.
Pages:
* NodesPage — wires hooks + WebSocket together, renders summary card
+ NodeList, hosts the form modal. Uses Modal.useModal() for the
delete confirm so the dialog inherits ConfigProvider theming.
* NodeList — desktop renders a Table with expandable history rows;
mobile flips to a vertical card list whose actions live in a
bottom-right Dropdown. The IP-blur eye toggle persists across both.
* NodeFormModal — controlled form (useState object, single setForm
per change). The reset-on-open effect computes the next state
once and applies it with eslint-disable to satisfy the new
react-hooks/set-state-in-effect rule on a legitimate pattern.
* NodeHistoryPanel — polls /panel/api/nodes/history/{id}/{metric}/
{bucket} every 15s, renders cpu+mem sparklines side-by-side.
* refactor(frontend): port settings to react+ts
Step 5 of the planned vue->react migration. Settings is the first
entry whose state model didn't translate to the Vue-style "parent
passes a reactive object, children mutate it in place" pattern, so
the React port flips it to lifted state + a typed updateSetting
patch function.
* models/setting.ts — typed AllSetting class with the same field
defaults and equals() behavior the vue version had. The .js
twin is deleted; nothing else imported it.
* hooks/useAllSetting.ts — owns allSetting + oldAllSetting state,
exposes updateSetting(patch), saveDisabled is derived via useMemo
off equals() (no more 1Hz dirty-check timer).
* components/SettingListItem.tsx — children-based wrapper instead
of named slots. The vue twin stays alive because xray (BasicsTab,
DnsTab) still imports it; deleted when xray migrates.
The five tab components and the TwoFactorModal each accept
{ allSetting, updateSetting } and render with AntD v5's Collapse
items[] API. Every v-model:value="x" became
value={...} onChange={(e) => updateSetting({ key: e.target.value })}
or onChange={(v) => updateSetting({ key: v })} for non-input
controls.
SubscriptionFormatsTab is the trickiest — fragment / noises[] /
mux / direct routing rules are stored as JSON-encoded strings on
the wire. Parsing them once via useMemo per field, mutating the
parsed object on edit, and stringifying back into the patch keeps
the round-trip identical to the vue version.
SettingsPage hosts the tab navigation (with hash sync), the
save / restart action bar, the security-warnings alert banner,
and the restart flow that rebuilds the panel URL after the new
host/port/cert settings take effect.
* refactor(frontend): port clients to react+ts
Step 6 of the planned vue->react migration. Clients is the biggest
data-CRUD page in the panel (1.1k-line ClientsPage, 4 modals, full
table + mobile card list, WebSocket-driven realtime traffic + online
updates).
New shared infra (lives alongside vue twins until inbounds migrates):
* hooks/useClients.ts — clients + inbounds list, CRUD + bulk delete +
attach/detach + traffic reset, with WebSocket event handlers
(traffic, client_stats, invalidate) and a small debounced refresh
on the invalidate event. State managed via setState; the live
client_stats event merges traffic snapshots row-by-row through a
ref to avoid stale closure issues.
* hooks/useDatepicker.ts — singleton "gregorian"/"jalalian" cache
with subscribe/notify so multiple components can read the panel's
Calendar Type without re-fetching. Mirrors useDatepicker.js.
* components/DateTimePicker.tsx — AntD DatePicker wrapper.
vue3-persian-datetime-picker has no React port; the Jalali UI
calendar is deferred (read-only Jalali display via IntlUtil
formatDate still works). The vue twin stays for inbounds.
* pages/inbounds/QrPanel.tsx — copy/download/copy-as-png QR helper
shared between clients (qr modal) and inbounds (still on vue).
Vue twin stays alive at QrPanel.vue.
* models/inbound.ts — slim port: only the TLS_FLOW_CONTROL constant
the clients form needs. The full inbound model stays as
inbound.js for now; inbounds will pull it in as inbound.ts.
The clients page itself uses Modal.useModal() for all confirm
dialogs (delete, bulk-delete, reset-traffic, delDepleted, reset-all)
so the dialogs render themed. Filter state persists to
localStorage under clientsFilterState. Sort + pagination state is
local; pageSize seeds from /panel/setting/defaultSettings.
The four modals share a controlled "open/onOpenChange" pattern
that replaces vue's v-model:open. ClientFormModal computes
attach/detach diffs from the inbound multi-select on submit; the
parent's onSave callback routes them through useClients's attach()/
detach() after the main update succeeds.
ESLint config: turned off four react-hooks v7 rules
(react-compiler, preserve-manual-memoization, set-state-in-effect,
purity). They're all React-Compiler-driven informational rules; we
don't run the compiler and the patterns they flag (initial-fetch
useEffect, derived computations using Date.now, inline arrow event
handlers) are all idiomatic React. Disabling globally instead of
per-line keeps the diff readable.
* refactor(frontend): port index dashboard to react+ts
Step 7 of the Vue→React migration. Ports the overview/index entry: dashboard
page, status + xray cards, panel-update / log / backup / system-history /
xray-metrics / xray-log / version modals, and the custom-geo subsection. Adds
the shared JsonEditor (CodeMirror 6) and useStatus hook used by the config
modal. Removes the unused react-hooks/set-state-in-effect disables now that
the rule is off globally.
* refactor(frontend): port xray to react+ts
Step 8 of the Vue→React migration. Ports the xray config entry: page shell,
basics/routing/outbounds/balancers/dns tabs, the rule + balancer + dns server
+ dns presets + warp + nord modals, the protocol-aware outbound form, and the
shared FinalMaskForm (TCP/UDP masks + QUIC params). Adds useXraySetting that
mirrors the legacy two-way sync between the JSON template string and the
parsed templateSettings tree. The outbound model itself stays in JS so the
class-driven form keeps its existing mutation API; instance access is typed
loosely inside the form to match.
The shared FinalMaskForm.vue and JsonEditor.vue stay alongside the new .tsx
versions until step 9 — InboundFormModal.vue still imports them.
Adds react-hooks/immutability and react-hooks/refs to the already-disabled
react-compiler rule set; both flag the outbound form's instance-mutation
pattern that doesn't run through useState.
* Upgrade frontend deps (antd v6, i18n, TS)
Bump frontend dependencies in package.json and regenerate package-lock.json. Notable updates: upgrade antd to v6, update i18next/react-i18next, axios, qs, vue-i18n, TypeScript and ESLint, plus related @rc-component packages and replacements (e.g. classnames/rc-util -> clsx/@rc-component/util). Lockfile changes reflect the new dependency tree required for Ant Design v6 and other package upgrades.
* refactor(frontend): port inbounds to react+ts and drop vue toolchain
Step 9 — the last entry. Ports the inbounds entry: page shell, list with
desktop table + mobile cards, info modal, qr-code modal, share-link
helpers, and the protocol-aware form modal (basics / protocol /
stream / security / sniffing / advanced JSON). useInbounds replaces
the Vue composable with WebSocket-driven traffic + client-stats merge.
Inbound and DBInbound models stay in JS so the class-driven form keeps
its mutation API; instance access is typed loosely inside the form to
match. FinalMaskForm/JsonEditor/TextModal/PromptModal/InfinityIcon are
the last shared bits to flip; their .vue counterparts go too.
Toolchain cleanup now that no entry needs Vue: drop plugin-vue from
vite.config, remove the .vue lint block + parser, prune vue / vue-i18n
/ ant-design-vue / @ant-design/icons-vue / vue3-persian-datetime-picker
/ moment-jalaali override from package.json, and switch utils/index.js
to import { message } from 'antd' instead of ant-design-vue.
* chore(frontend): adopt antd v6 api updates
Sweep deprecated props across the React tree:
- Modal: destroyOnClose -> destroyOnHidden, maskClosable -> mask.closable
- Space: direction -> orientation (or removed when redundant)
- Input.Group compact -> Space.Compact block
- Drawer: width -> size
- Spin: tip -> description
- Progress: trailColor -> railColor
- Alert: message -> title
- Popover: overlayClassName -> rootClassName
- BackTop -> FloatButton.BackTop
Also refresh dashboard theming for v6: rename dark/ultra Layout and Menu
tokens (siderBg, darkItemBg, darkSubMenuItemBg, darkPopupBg), tweak gauge
size/stroke, add font-size overrides for Statistic and Progress so the
overview numbers stay legible under v6 defaults.
* chore(frontend): antd v6 polish, theme + modal fixes
- adopt message.useMessage hook + messageBus bridge so HttpUtil messages
inherit ConfigProvider theme tokens
- replace deprecated antd APIs (List, Input addonBefore/After, Empty
imageStyle); introduce InputAddon helper + SettingListItem custom rows
- fix dark/ultra selectors in portaled modals (body.dark,
html[data-theme='ultra-dark']) instead of nonexistent .is-dark/.is-ultra
- add horizontal scroll to clients table; reorder node columns so
actions+enable sit at the left
- swap raw button for antd Button in NodeFormModal test connection
- fix FinalMaskForm nested-form by hoisting it outside OutboundFormModal's
parent Form
- fix advanced "all" JSON tab in InboundFormModal — useMemo on a mutated
ref was stale; compute on every render
- fix chart-on-open for SystemHistory + XrayMetrics modals by adding open
to effect deps (useRef.current doesn't trigger re-runs)
- switch i18next interpolation to single-brace {var} to match locale files
- drop residual Vue mentions in CI workflows and Go comments
* fix(frontend): qr code collapse — open only first panel, allow toggle
ClientQrModal and QrCodeModal both used activeKey without onChange,
forcing every panel open and blocking user toggle. Switch to controlled
state initialized to the first item's key on open, with onChange so
clicks update state.
Also remove unused AppBridge.tsx (superseded by per-page message.useMessage
hook).
* fix(frontend): hover cards, balancer load, routing dnd, modal a11y, outbound crash
- ClientsPage/SettingsPage/XrayPage: add hoverable to bottom card/tabs so
hover affordance matches the top card
- BalancerFormModal: lazy-init useState from props + destroyOnHidden so
the form mounts with saved values instead of relying on a useEffect
sync that could miss the first open
- RoutingTab: rewrite pointer drag — handlers are now defined inside the
pointerdown closure so addEventListener/removeEventListener match;
drag state lives on a ref (from/to/moved) so onUp reads the real
indices, not stale closure values. Adds setPointerCapture so Windows
and touch keep delivering events when the cursor leaves the handle.
- OutboundFormModal/InboundFormModal: blur the focused input before
switching tabs to silence the aria-hidden-on-focused-element warning
- utils.isArrEmpty: return true for undefined/null arrays — the old form
treated undefined as "not empty" which crashed VLESSSettings.fromJson
when json.vnext was missing
* fix(frontend): clipboard reliability + restyle login page
- ClipboardManager.copyText: prefer navigator.clipboard on secure
contexts, fall back to a focused on-screen textarea + execCommand.
Old path used left:-9999px which failed selection in some browsers
and swallowed execCommand's return value, so the "copied" toast
appeared even when nothing made it to the clipboard.
- LoginPage: richer gradient backdrop — five animated colour blobs,
glassmorphic card (backdrop-filter blur + saturate), gradient brand
text/accent, masked grid texture for depth, and a thin gradient
border on the card. Light/dark/ultra each get their own palette.
* Memoize compactAdvancedJson and update deps
Wrap compactAdvancedJson in useCallback (dependent on messageApi) and add it to the dependency array of applyAdvancedJsonToBasic. This ensures a stable function reference for correct dependency tracking and avoids stale closures/unnecessary re-renders in InboundFormModal.tsx.
* style(frontend): prettier charts, drop redundant frame, format net rates
- Sparkline: multi-stop gradient fill, soft drop-shadow under the line,
dashed grid, glowing pulse on the latest-point marker, pill-shaped
tooltip with dashed crosshair
- XrayMetricsModal: glow + pulse on the observatory alive dot,
monospace stamps/listen text
- SystemHistoryModal: keep just the modal's frame around the chart (the
inner wrapper I'd added stacked a second border on top); strip the
decimal from Net Up/Down (25.63 KB/s → 25 KB/s) only on this chart's
formatter
* style(frontend): refined dark/ultra palette + shared pro card frame
- Dark tokens shifted to a cooler, Linear-style palette: page #1a1b1f,
sidebar/header #15161a (recessed nav, darker than cards), card
#23252b, elevated #2d2f37
- Ultra dark: page pure #000 for OLED, sidebar #050507 disappears into
the frame, card #101013 with a clear step, elevated #1a1a1e
- New styles/page-cards.css holds the card border/shadow/hover rules so
all seven content pages (index, clients, inbounds, xray, settings,
nodes, api-docs) share one definition instead of duplicating in each
page CSS
- Dashboard typography: uppercase card titles with letter-spacing,
larger 17px stat values, subtle gradient divider between stat columns,
ellipsis on action labels so "Backup & Restore" doesn't break the
card height at mid widths
- Light --bg-page stays at #e6e8ec for the contrast against white cards
* fix(frontend): wireguard info alignment, blue login dark, embed gitkeep
- align WireGuard info-modal fields with Protocol/Address/Port by wrapping
values in Tag (matches the rest of the dl.info-list rows)
- swap login dark palette from purple to pure blue blobs/accent/brand
- pin web/dist/.gitkeep through gitignore so //go:embed all:dist never
fails on a fresh clone with an empty dist directory
* docs: refresh frontend docs for the React + TS + AntD 6 stack
Update CONTRIBUTING.md and frontend/README.md to describe the migrated
frontend accurately:
- replace Vue 3 / Ant Design Vue 4 references with React 19 / AntD 6 / TS
- swap composables -> hooks, vue-i18n -> react-i18next, createApp -> createRoot
- mention the typecheck step (tsc --noEmit) in the PR checklist
- document the Vite 8.0.13 pin and TypeScript strict mode in conventions
- list the nodes and api-docs entries that were missing from the layout
* style(frontend): improve readability and mobile polish
- bump statistic title/value contrast in dark and ultra-dark so totals
on the inbounds summary card stay legible
- give index card actions explicit colors per theme so links like Stop,
Logs, System History no longer fade into the card background
- show the panel version as a tag next to "3X-UI" on mobile, mirroring
the Xray version tag pattern, and turn it orange when an update is
available
- make the login settings button a proper circle by adding size="large"
+ an explicit border-radius fallback on .toolbar-btn
* feat: jalali calendar support and date formatting fixes
- Wire useDatepicker into IntlUtil and switch jalalian display locale
to fa-IR for clean "1405/07/03 12:00:00" output (drops the awkward
"AP" era suffix that "<lang>-u-ca-persian" produced)
- Drop in persian-calendar-suite for the jalali date picker, with a
light/dark/ultra theme map and CSS overrides so the inline-styled
input stays readable and bg matches the surrounding container
- Force LTR on the picker input so "1405/03/07 00:00" reads naturally
- Pass calendar setting through ClientInfoModal, ClientsPage Duration
tooltip, and ClientFormModal's expiry picker
- Heuristic toMs() in ClientInfoModal so GORM's autoUpdateTime seconds
render as a real date instead of "1348/11/01"
- Persist UpdatedAt on the ClientRecord row in client_service.Update;
previously only the inbound settings JSON was bumped, so the panel
never saw a fresh updated_at after editing a client
* feat(frontend): donate link, panel version label, login lang menu
- Sidebar: add heart donate link to https://donate.sanaei.dev and small panel version under 3X-UI brand
- Login: swap settings-cog for translation icon, drop title, render languages as a direct list
- Vite dev: inject window.X_UI_CUR_VER from config/version so dev mode matches prod
- Translations: add menu.donate across all locales
* fix(xray-update): respect XUI_BIN_FOLDER on Windows
The Windows update path hardcoded "bin/xray-windows-amd64.exe", ignoring
the configured XUI_BIN_FOLDER. In dev mode (folder set to x-ui) this
created a stray bin/ folder while the running binary stayed un-updated.
* Bump Xray to v26.5.9 and minor cleanup
Update Xray release URLs to v26.5.9 in the GitHub Actions workflow and DockerInit.sh. Remove the hardcoded skip for tagVersion "26.5.3" so it will be considered when collecting Xray versions. Apply small formatting fixes: remove an extra blank line in database/db.go, normalize spacing/alignment of Protocol constants in database/model/model.go, and trim a trailing blank line in web/controller/inbound.go.
* fix(frontend): route remaining copy buttons through ClipboardManager
Direct navigator.clipboard calls fail in non-secure contexts (HTTP on a
LAN IP), making the API-docs code copy and security-tab token copy
silently broken. Both now go through ClipboardManager which falls back
to document.execCommand('copy') when navigator.clipboard is unavailable.
* fix(db): store CreatedAt/UpdatedAt in milliseconds
GORM's autoCreateTime/autoUpdateTime tags default to Unix seconds on
int64 fields and overwrite the service-supplied UnixMilli value on
save. The frontend interprets these timestamps as JS Date inputs
(milliseconds), so created/updated columns rendered ~1970 dates. Adding
the :milli qualifier makes GORM match what the service code and UI
expect.
* Improve legacy clipboard copy handling
Refactor ClipboardManager._legacyCopy to better handle focus and selection when copying. The textarea is now appended to the active element's parent (or body) and placed off-screen with aria-hidden and readonly attributes. The code preserves and restores the previous document selection and active element, uses focus({preventScroll: true}) to avoid scrolling, and returns the execCommand('copy') result. This makes legacy copy behavior more robust and less disruptive to the page state.
* fix(lint): drop redundant ok=false in clipboard fallback catch
* chore(deps): bump golang.org/x/net to v0.55.0 for GO-2026-5026
2026-05-23 13:21:45 +00:00
|
|
|
|
"link": "管理",
|
|
|
|
|
|
"donate": "捐贈"
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
},
|
|
|
|
|
|
"pages": {
|
|
|
|
|
|
"login": {
|
|
|
|
|
|
"hello": "你好",
|
|
|
|
|
|
"title": "歡迎",
|
|
|
|
|
|
"loginAgain": "登入時效已過,請重新登入",
|
|
|
|
|
|
"toasts": {
|
|
|
|
|
|
"invalidFormData": "資料格式錯誤",
|
|
|
|
|
|
"emptyUsername": "請輸入使用者名稱",
|
|
|
|
|
|
"emptyPassword": "請輸入密碼",
|
|
|
|
|
|
"wrongUsernameOrPassword": "用戶名、密碼或雙重驗證碼無效。",
|
|
|
|
|
|
"successLogin": "您已成功登入您的帳戶。"
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
"index": {
|
|
|
|
|
|
"title": "系統狀態",
|
|
|
|
|
|
"cpu": "CPU",
|
|
|
|
|
|
"logicalProcessors": "邏輯處理器",
|
|
|
|
|
|
"frequency": "頻率",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"swap": "Swap",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"storage": "儲存",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"memory": "RAM",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"threads": "執行緒",
|
|
|
|
|
|
"xrayStatus": "Xray",
|
|
|
|
|
|
"stopXray": "停止",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"restartXray": "重新啟動",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"xraySwitch": "版本",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"xrayUpdates": "Xray 更新",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"xraySwitchClick": "選擇你要切換到的版本",
|
|
|
|
|
|
"xraySwitchClickDesk": "請謹慎選擇,因為較舊版本可能與當前配置不相容",
|
|
|
|
|
|
"updatePanel": "更新面板",
|
|
|
|
|
|
"panelUpdateDesc": "這將把 3X-UI 更新到最新版本並重新啟動面板服務。",
|
|
|
|
|
|
"currentPanelVersion": "目前面板版本",
|
|
|
|
|
|
"latestPanelVersion": "最新面板版本",
|
|
|
|
|
|
"panelUpToDate": "面板已是最新",
|
|
|
|
|
|
"upToDate": "已是最新",
|
|
|
|
|
|
"xrayStatusUnknown": "未知",
|
|
|
|
|
|
"xrayStatusRunning": "運行中",
|
|
|
|
|
|
"xrayStatusStop": "停止",
|
|
|
|
|
|
"xrayStatusError": "錯誤",
|
|
|
|
|
|
"xrayErrorPopoverTitle": "執行Xray時發生錯誤",
|
|
|
|
|
|
"operationHours": "系統正常執行時間",
|
|
|
|
|
|
"systemHistoryTitle": "系統歷史",
|
2026-05-12 00:17:45 +00:00
|
|
|
|
"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": "最後嘗試",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"trendLast2Min": "最近 2 分鐘",
|
|
|
|
|
|
"systemLoad": "系統負載",
|
|
|
|
|
|
"systemLoadDesc": "過去 1、5 和 15 分鐘的系統平均負載",
|
|
|
|
|
|
"connectionCount": "連線數",
|
|
|
|
|
|
"ipAddresses": "IP地址",
|
|
|
|
|
|
"toggleIpVisibility": "切換IP可見性",
|
|
|
|
|
|
"overallSpeed": "整體速度",
|
|
|
|
|
|
"upload": "上傳",
|
|
|
|
|
|
"download": "下載",
|
|
|
|
|
|
"totalData": "總數據",
|
|
|
|
|
|
"sent": "已發送",
|
|
|
|
|
|
"received": "已接收",
|
|
|
|
|
|
"documentation": "文件",
|
|
|
|
|
|
"xraySwitchVersionDialog": "您確定要變更Xray版本嗎?",
|
|
|
|
|
|
"xraySwitchVersionDialogDesc": "這將會把Xray版本變更為#version#。",
|
|
|
|
|
|
"xraySwitchVersionPopover": "Xray 更新成功",
|
|
|
|
|
|
"panelUpdateDialog": "您確定要更新面板嗎?",
|
|
|
|
|
|
"panelUpdateDialogDesc": "這將把 3X-UI 更新到 #version# 並重新啟動面板服務。",
|
|
|
|
|
|
"panelUpdateCheckPopover": "面板更新檢查失敗",
|
|
|
|
|
|
"panelUpdateStartedPopover": "面板更新已開始",
|
|
|
|
|
|
"geofileUpdateDialog": "您確定要更新地理檔案嗎?",
|
|
|
|
|
|
"geofileUpdateDialogDesc": "這將更新 #filename# 檔案。",
|
|
|
|
|
|
"geofilesUpdateDialogDesc": "這將更新所有文件。",
|
|
|
|
|
|
"geofilesUpdateAll": "全部更新",
|
|
|
|
|
|
"geofileUpdatePopover": "地理檔案更新成功",
|
|
|
|
|
|
"customGeoTitle": "自訂 GeoSite / GeoIP",
|
|
|
|
|
|
"customGeoAdd": "新增",
|
|
|
|
|
|
"customGeoType": "類型",
|
|
|
|
|
|
"customGeoAlias": "別名",
|
|
|
|
|
|
"customGeoUrl": "URL",
|
|
|
|
|
|
"customGeoEnabled": "啟用",
|
|
|
|
|
|
"customGeoLastUpdated": "上次更新",
|
|
|
|
|
|
"customGeoExtColumn": "路由 (ext:…)",
|
|
|
|
|
|
"customGeoToastUpdateAll": "所有自訂來源已更新",
|
|
|
|
|
|
"customGeoActions": "操作",
|
|
|
|
|
|
"customGeoEdit": "編輯",
|
|
|
|
|
|
"customGeoDelete": "刪除",
|
|
|
|
|
|
"customGeoDownload": "立即更新",
|
|
|
|
|
|
"customGeoModalAdd": "新增自訂 geo",
|
|
|
|
|
|
"customGeoModalEdit": "編輯自訂 geo",
|
|
|
|
|
|
"customGeoModalSave": "儲存",
|
|
|
|
|
|
"customGeoDeleteConfirm": "刪除此自訂 geo 來源?",
|
|
|
|
|
|
"customGeoRoutingHint": "在路由規則中將值欄寫為 ext:檔案.dat:標籤(替換標籤)。",
|
|
|
|
|
|
"customGeoInvalidId": "無效的資源 ID",
|
|
|
|
|
|
"customGeoAliasesError": "載入自訂 geo 別名失敗",
|
|
|
|
|
|
"customGeoValidationAlias": "別名只能包含小寫字母、數字、- 和 _",
|
|
|
|
|
|
"customGeoValidationUrl": "URL 必須以 http:// 或 https:// 開頭",
|
|
|
|
|
|
"customGeoAliasPlaceholder": "a-z 0-9 _ -",
|
|
|
|
|
|
"customGeoAliasLabelSuffix": "(自訂)",
|
|
|
|
|
|
"customGeoToastList": "自訂 geo 清單",
|
|
|
|
|
|
"customGeoToastAdd": "新增自訂 geo",
|
|
|
|
|
|
"customGeoToastUpdate": "更新自訂 geo",
|
|
|
|
|
|
"customGeoToastDelete": "自訂 geofile「{{ .fileName }}」已刪除",
|
|
|
|
|
|
"customGeoToastDownload": "geofile「{{ .fileName }}」已更新",
|
|
|
|
|
|
"customGeoErrInvalidType": "類型必須是 geosite 或 geoip",
|
|
|
|
|
|
"customGeoErrAliasRequired": "請填寫別名",
|
|
|
|
|
|
"customGeoErrAliasPattern": "別名包含不允許的字元",
|
|
|
|
|
|
"customGeoErrAliasReserved": "此別名已保留",
|
|
|
|
|
|
"customGeoErrUrlRequired": "請填寫 URL",
|
|
|
|
|
|
"customGeoErrInvalidUrl": "URL 無效",
|
|
|
|
|
|
"customGeoErrUrlScheme": "URL 必須使用 http 或 https",
|
|
|
|
|
|
"customGeoErrUrlHost": "URL 主機無效",
|
|
|
|
|
|
"customGeoErrDuplicateAlias": "此類型已使用該別名",
|
|
|
|
|
|
"customGeoErrNotFound": "找不到自訂 geo 來源",
|
|
|
|
|
|
"customGeoErrDownload": "下載失敗",
|
|
|
|
|
|
"customGeoErrUpdateAllIncomplete": "有一個或多個自訂 geo 來源更新失敗",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"customGeoEmpty": "尚無自訂 geo 來源 — 點擊「新增」以建立",
|
|
|
|
|
|
"dontRefresh": "安裝中,請勿重新整理此頁面",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"logs": "記錄",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"config": "配置",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"backup": "備份",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"backupTitle": "備份和恢復",
|
|
|
|
|
|
"exportDatabase": "備份",
|
|
|
|
|
|
"exportDatabaseDesc": "點擊下載包含當前資料庫備份的 .db 文件到您的設備。",
|
|
|
|
|
|
"importDatabase": "恢復",
|
|
|
|
|
|
"importDatabaseDesc": "點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。",
|
|
|
|
|
|
"importDatabaseSuccess": "資料庫匯入成功",
|
|
|
|
|
|
"importDatabaseError": "匯入資料庫時發生錯誤",
|
|
|
|
|
|
"readDatabaseError": "讀取資料庫時發生錯誤",
|
|
|
|
|
|
"getDatabaseError": "檢索資料庫時發生錯誤",
|
2026-05-31 15:53:34 +00:00
|
|
|
|
"getConfigError": "檢索設定檔時發生錯誤",
|
|
|
|
|
|
"backupPostgresNote": "此面板執行於 PostgreSQL 上。「備份」會下載一個 pg_dump 封存檔(.dump),「還原」會透過 pg_restore 重新載入。伺服器需要安裝 PostgreSQL 用戶端工具(pg_dump 與 pg_restore)。",
|
|
|
|
|
|
"exportDatabasePgDesc": "點擊將目前資料庫的 PostgreSQL 傾印(.dump)下載到您的裝置。",
|
|
|
|
|
|
"importDatabasePgDesc": "點擊選擇並上傳 .dump 檔案以還原您的 PostgreSQL 資料庫。此操作將取代所有目前的資料。"
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
},
|
|
|
|
|
|
"inbounds": {
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"title": "入站",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"totalDownUp": "總上傳 / 下載",
|
|
|
|
|
|
"totalUsage": "總用量",
|
|
|
|
|
|
"inboundCount": "入站數量",
|
|
|
|
|
|
"operate": "選單",
|
|
|
|
|
|
"enable": "啟用",
|
|
|
|
|
|
"remark": "備註",
|
|
|
|
|
|
"node": "節點",
|
|
|
|
|
|
"deployTo": "部署到",
|
|
|
|
|
|
"localPanel": "本機面板",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"fallbacks": {
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"title": "Fallbacks",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"help": "當此入站的連線未匹配任何用戶時,將其路由到另一個入站。在下方選擇一個子入站,路由欄位(SNI / ALPN / Path / xver)會自動從子入站的傳輸方式填入——大多數情境不需要再調整。每個子入站應監聽 127.0.0.1,security=none。",
|
|
|
|
|
|
"empty": "尚未新增回落",
|
|
|
|
|
|
"add": "新增回落",
|
|
|
|
|
|
"pickInbound": "選擇一個入站",
|
|
|
|
|
|
"matchAny": "任何",
|
2026-05-28 19:17:49 +00:00
|
|
|
|
"destPlaceholder": "自動(子入站 listen:port)",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"rederive": "從子入站重新填入",
|
|
|
|
|
|
"rederived": "已從子入站重新填入",
|
|
|
|
|
|
"editAdvanced": "編輯路由欄位",
|
|
|
|
|
|
"hideAdvanced": "隱藏進階",
|
|
|
|
|
|
"quickAddAll": "一鍵新增所有符合的入站",
|
|
|
|
|
|
"quickAdded": "已新增 {n} 個回落",
|
|
|
|
|
|
"quickAddedNone": "沒有可新增的新入站",
|
|
|
|
|
|
"routesWhen": "當條件成立時路由",
|
|
|
|
|
|
"defaultCatchAll": "預設 — 兜底匹配其餘"
|
|
|
|
|
|
},
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"protocol": "協議",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"port": "連接埠",
|
|
|
|
|
|
"portMap": "連接埠對應",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"traffic": "流量",
|
|
|
|
|
|
"details": "詳細資訊",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"transportConfig": "傳輸",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"expireDate": "到期時間",
|
|
|
|
|
|
"createdAt": "建立時間",
|
|
|
|
|
|
"updatedAt": "更新時間",
|
|
|
|
|
|
"resetTraffic": "重置流量",
|
|
|
|
|
|
"addInbound": "新增入站",
|
|
|
|
|
|
"generalActions": "通用操作",
|
|
|
|
|
|
"modifyInbound": "修改入站",
|
|
|
|
|
|
"deleteInbound": "刪除入站",
|
|
|
|
|
|
"deleteInboundContent": "確定要刪除入站嗎?",
|
2026-05-23 17:43:21 +00:00
|
|
|
|
"deleteConfirmTitle": "刪除入站「{remark}」?",
|
|
|
|
|
|
"deleteConfirmContent": "將刪除此入站及其所有客戶端。此操作無法復原。",
|
|
|
|
|
|
"resetConfirmTitle": "重置「{remark}」的流量?",
|
|
|
|
|
|
"resetConfirmContent": "將此入站的上/下行計數器歸零。",
|
2026-05-30 22:29:24 +00:00
|
|
|
|
"selectedCount": "已選 {count} 項",
|
|
|
|
|
|
"selectAll": "全選",
|
|
|
|
|
|
"bulkDeleteConfirmTitle": "刪除 {count} 個入站?",
|
|
|
|
|
|
"bulkDeleteConfirmContent": "將刪除所選入站及其所有客戶端。此操作無法復原。",
|
2026-05-23 17:43:21 +00:00
|
|
|
|
"cloneConfirmTitle": "複製入站「{remark}」?",
|
|
|
|
|
|
"cloneConfirmContent": "使用新連接埠和空客戶端清單建立副本。",
|
2026-05-27 16:17:44 +00:00
|
|
|
|
"delAllClients": "刪除所有客戶端",
|
|
|
|
|
|
"delAllClientsConfirmTitle": "從「{remark}」中刪除全部 {count} 個客戶端?",
|
|
|
|
|
|
"delAllClientsConfirmContent": "從此入站中移除每個客戶端並捨棄其流量記錄。入站本身將保留。此操作無法復原。",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"attachClients": "附加客戶端到…",
|
|
|
|
|
|
"addClientsToGroup": "將客戶端加入群組…",
|
|
|
|
|
|
"attachClientsTitle": "從「{remark}」附加客戶端",
|
|
|
|
|
|
"attachClientsDesc": "將相同的 {count} 個客戶端(相同 UUID/密碼與共享流量)附加到選定入站。它們仍保留於此入站。",
|
|
|
|
|
|
"attachClientsTargets": "目標入站",
|
|
|
|
|
|
"attachClientsNoTargets": "沒有可附加的其他相容入站。",
|
|
|
|
|
|
"attachClientsResult": "已附加 {attached},已略過 {skipped}。",
|
|
|
|
|
|
"attachClientsResultMixed": "已附加 {attached},已略過 {skipped},錯誤 {errors}。",
|
|
|
|
|
|
"attachClientsSelectLabel": "要附加的客戶端",
|
|
|
|
|
|
"attachClientsSearchPlaceholder": "搜尋電子郵件或備註",
|
|
|
|
|
|
"attachClientsStatusDisabled": "已停用",
|
|
|
|
|
|
"attachClientsSelectedCount": "已選 {selected}/{total}",
|
|
|
|
|
|
"detachClients": "分離客戶端",
|
|
|
|
|
|
"detachClientsTitle": "從「{remark}」分離客戶端",
|
|
|
|
|
|
"detachClientsDesc": "僅從此入站移除選取的客戶端。客戶端記錄保留(用 Delete 完全移除)。來源共有 {count} 個客戶端。",
|
|
|
|
|
|
"detachClientsResult": "已分離 {detached},已略過 {skipped}。",
|
|
|
|
|
|
"detachClientsResultMixed": "已分離 {detached},已略過 {skipped},錯誤 {errors}。",
|
|
|
|
|
|
"detachClientsSelectLabel": "要分離的客戶端",
|
2026-05-23 17:43:21 +00:00
|
|
|
|
"exportLinksTitle": "匯出入站連結",
|
|
|
|
|
|
"exportSubsTitle": "匯出訂閱連結",
|
|
|
|
|
|
"exportAllLinksTitle": "匯出所有入站連結",
|
|
|
|
|
|
"exportAllSubsTitle": "匯出所有訂閱連結",
|
2026-05-28 16:45:59 +00:00
|
|
|
|
"exportAllLinksFileName": "所有入站",
|
|
|
|
|
|
"exportAllSubsFileName": "所有入站-Subs",
|
2026-05-23 17:43:21 +00:00
|
|
|
|
"inboundJsonTitle": "入站 JSON",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"deleteClient": "刪除客戶端",
|
|
|
|
|
|
"deleteClientContent": "確定要刪除客戶端嗎?",
|
|
|
|
|
|
"resetTrafficContent": "確定要重置流量嗎?",
|
|
|
|
|
|
"copyLink": "複製連結",
|
|
|
|
|
|
"address": "地址",
|
|
|
|
|
|
"network": "網路",
|
|
|
|
|
|
"destinationPort": "目標埠",
|
|
|
|
|
|
"targetAddress": "目標地址",
|
|
|
|
|
|
"monitorDesc": "留空表示監聽所有 IP",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"meansNoLimit": "= 無限制。(單位: GB)",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"totalFlow": "總流量",
|
|
|
|
|
|
"leaveBlankToNeverExpire": "留空表示永不過期",
|
|
|
|
|
|
"noRecommendKeepDefault": "建議保留預設值",
|
|
|
|
|
|
"certificatePath": "檔案路徑",
|
|
|
|
|
|
"certificateContent": "檔案內容",
|
|
|
|
|
|
"publicKey": "公鑰",
|
|
|
|
|
|
"privatekey": "私鑰",
|
|
|
|
|
|
"clickOnQRcode": "點選二維碼複製",
|
|
|
|
|
|
"client": "客戶",
|
|
|
|
|
|
"export": "匯出連結",
|
|
|
|
|
|
"clone": "複製",
|
|
|
|
|
|
"cloneInbound": "複製",
|
|
|
|
|
|
"cloneInboundContent": "此入站規則除埠(Port)、監聽 IP(Listening IP)和客戶端(Clients)以外的所有配置都將應用於克隆",
|
|
|
|
|
|
"cloneInboundOk": "建立克隆",
|
|
|
|
|
|
"resetAllTraffic": "重置所有入站流量",
|
|
|
|
|
|
"resetAllTrafficTitle": "重置所有入站流量",
|
|
|
|
|
|
"resetAllTrafficContent": "確定要重置所有入站流量嗎?",
|
|
|
|
|
|
"resetInboundClientTraffics": "重置客戶端流量",
|
|
|
|
|
|
"resetInboundClientTrafficTitle": "重置所有客戶端流量",
|
|
|
|
|
|
"resetInboundClientTrafficContent": "確定要重置此入站客戶端的所有流量嗎?",
|
|
|
|
|
|
"resetAllClientTraffics": "重置所有客戶端流量",
|
|
|
|
|
|
"resetAllClientTrafficTitle": "重置所有客戶端流量",
|
|
|
|
|
|
"resetAllClientTrafficContent": "確定要重置所有客戶端的所有流量嗎?",
|
|
|
|
|
|
"delDepletedClients": "刪除流量耗盡的客戶端",
|
|
|
|
|
|
"delDepletedClientsTitle": "刪除流量耗盡的客戶端",
|
|
|
|
|
|
"delDepletedClientsContent": "確定要刪除所有流量耗盡的客戶端嗎?",
|
|
|
|
|
|
"email": "電子郵件",
|
|
|
|
|
|
"emailDesc": "電子郵件必須完全唯一",
|
|
|
|
|
|
"IPLimit": "IP 限制",
|
|
|
|
|
|
"IPLimitDesc": "如果數量超過設定值,則禁用入站流量。(0 = 禁用)",
|
|
|
|
|
|
"IPLimitlog": "IP 日誌",
|
|
|
|
|
|
"IPLimitlogDesc": "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)",
|
|
|
|
|
|
"IPLimitlogclear": "清除日誌",
|
|
|
|
|
|
"setDefaultCert": "從面板設定證書",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"setDefaultCertEmpty": "面板尚未設定憑證。請先在「設定」中設定。",
|
2026-05-28 16:45:59 +00:00
|
|
|
|
"streamTab": "傳輸",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"securityTab": "安全",
|
2026-05-28 16:45:59 +00:00
|
|
|
|
"sniffingTab": "嗅探",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"sniffingMetadataOnly": "僅中繼資料",
|
|
|
|
|
|
"sniffingRouteOnly": "僅路由",
|
|
|
|
|
|
"sniffingIpsExcluded": "排除的 IP",
|
|
|
|
|
|
"sniffingDomainsExcluded": "排除的網域",
|
|
|
|
|
|
"decryption": "解密",
|
|
|
|
|
|
"encryption": "加密",
|
|
|
|
|
|
"vlessAuthX25519": "X25519 認證",
|
|
|
|
|
|
"vlessAuthMlkem768": "ML-KEM-768 認證",
|
|
|
|
|
|
"vlessAuthCustom": "自訂",
|
|
|
|
|
|
"vlessAuthSelected": "已選擇:{auth}",
|
|
|
|
|
|
"advanced": {
|
|
|
|
|
|
"title": "入站 JSON 部分",
|
|
|
|
|
|
"subtitle": "完整入站 JSON 以及針對 settings、sniffing 和 streamSettings 的專用編輯器。",
|
|
|
|
|
|
"all": "全部",
|
|
|
|
|
|
"allHelp": "在單一編輯器中編輯包含所有欄位的完整入站物件。",
|
|
|
|
|
|
"settings": "設定",
|
|
|
|
|
|
"settingsHelp": "Xray settings 區塊包裝:",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"sniffing": "Sniffing",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"sniffingHelp": "Xray sniffing 區塊包裝:",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"stream": "Stream",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"streamHelp": "Xray stream 區塊包裝:",
|
|
|
|
|
|
"jsonErrorPrefix": "進階 JSON"
|
|
|
|
|
|
},
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"telegramDesc": "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或({'@'}userinfobot",
|
|
|
|
|
|
"subscriptionDesc": "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。",
|
|
|
|
|
|
"same": "相同",
|
2026-05-29 21:22:49 +00:00
|
|
|
|
"inboundInfo": "入站資訊",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"exportInbound": "匯出入站規則",
|
|
|
|
|
|
"import": "匯入",
|
|
|
|
|
|
"importInbound": "匯入入站規則",
|
|
|
|
|
|
"periodicTrafficResetTitle": "流量重置",
|
|
|
|
|
|
"periodicTrafficResetDesc": "按指定間隔自動重置流量計數器",
|
|
|
|
|
|
"lastReset": "上次重置",
|
|
|
|
|
|
"periodicTrafficReset": {
|
|
|
|
|
|
"never": "從不",
|
|
|
|
|
|
"daily": "每日",
|
|
|
|
|
|
"weekly": "每週",
|
|
|
|
|
|
"monthly": "每月",
|
|
|
|
|
|
"hourly": "每小時"
|
|
|
|
|
|
},
|
|
|
|
|
|
"toasts": {
|
|
|
|
|
|
"obtain": "獲取",
|
|
|
|
|
|
"updateSuccess": "更新成功",
|
|
|
|
|
|
"logCleanSuccess": "日誌已清除",
|
|
|
|
|
|
"inboundsUpdateSuccess": "入站連接已成功更新",
|
|
|
|
|
|
"inboundUpdateSuccess": "入站連接已成功更新",
|
|
|
|
|
|
"inboundCreateSuccess": "入站連接已成功建立",
|
2026-05-30 22:29:24 +00:00
|
|
|
|
"bulkDeleted": "已刪除 {count} 個入站",
|
|
|
|
|
|
"bulkDeletedMixed": "已刪除 {ok} 個,失敗 {failed} 個",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"inboundDeleteSuccess": "入站連接已成功刪除",
|
|
|
|
|
|
"inboundClientAddSuccess": "已新增入站客戶端",
|
|
|
|
|
|
"inboundClientDeleteSuccess": "入站客戶端已刪除",
|
|
|
|
|
|
"inboundClientUpdateSuccess": "入站客戶端已更新",
|
|
|
|
|
|
"delDepletedClientsSuccess": "所有耗盡客戶端已刪除",
|
|
|
|
|
|
"resetAllClientTrafficSuccess": "客戶端所有流量已重置",
|
|
|
|
|
|
"resetAllTrafficSuccess": "所有流量已重置",
|
|
|
|
|
|
"resetInboundClientTrafficSuccess": "流量已重置",
|
2026-05-13 12:49:54 +00:00
|
|
|
|
"resetInboundTrafficSuccess": "入站流量已重置",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"trafficGetError": "取得流量資料時發生錯誤",
|
|
|
|
|
|
"getNewX25519CertError": "取得X25519憑證時發生錯誤。",
|
|
|
|
|
|
"getNewmldsa65Error": "取得mldsa65憑證時發生錯誤。",
|
|
|
|
|
|
"getNewVlessEncError": "取得VlessEnc憑證時發生錯誤。"
|
|
|
|
|
|
},
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"form": {
|
|
|
|
|
|
"moveUp": "上移",
|
|
|
|
|
|
"moveDown": "下移",
|
|
|
|
|
|
"addAll": "全部新增",
|
|
|
|
|
|
"addAllFallbackTooltip": "為每個尚未連線的符合條件入站新增一個 fallback 列",
|
|
|
|
|
|
"peers": "Peers",
|
|
|
|
|
|
"addPeer": "新增 peer",
|
|
|
|
|
|
"keepAlive": "Keep-alive",
|
|
|
|
|
|
"autoSystemRoutesTooltip": "僅 Windows。CIDR 會自動加入系統路由表,使匹配的流量通過 TUN。",
|
|
|
|
|
|
"autoOutboundsInterface": "自動出站介面",
|
|
|
|
|
|
"autoOutboundsInterfaceTooltip": "出站流量的實體介面。使用 'auto' 進行偵測;設定 Auto system routes 時自動啟用。",
|
|
|
|
|
|
"rewriteAddress": "改寫地址",
|
|
|
|
|
|
"rewritePort": "改寫連接埠",
|
|
|
|
|
|
"allowedNetwork": "允許的網路",
|
|
|
|
|
|
"followRedirect": "跟隨重新導向",
|
|
|
|
|
|
"accounts": "帳號",
|
|
|
|
|
|
"allowTransparent": "允許透明",
|
|
|
|
|
|
"encryptionMethod": "加密方法",
|
|
|
|
|
|
"visionTestseed": "Vision testseed",
|
|
|
|
|
|
"version": "版本",
|
|
|
|
|
|
"udpIdleTimeout": "UDP 閒置逾時 (s)",
|
|
|
|
|
|
"masquerade": "偽裝",
|
|
|
|
|
|
"type": "類型",
|
|
|
|
|
|
"upstreamUrl": "Upstream URL",
|
|
|
|
|
|
"rewriteHost": "改寫 Host",
|
|
|
|
|
|
"skipTlsVerify": "略過 TLS 驗證",
|
|
|
|
|
|
"directory": "目錄",
|
|
|
|
|
|
"statusCode": "狀態碼",
|
|
|
|
|
|
"body": "Body",
|
|
|
|
|
|
"headers": "標頭",
|
|
|
|
|
|
"proxyProtocol": "Proxy Protocol",
|
|
|
|
|
|
"requestVersion": "請求版本",
|
|
|
|
|
|
"requestMethod": "請求方法",
|
|
|
|
|
|
"requestPath": "請求路徑",
|
|
|
|
|
|
"requestHeaders": "請求標頭",
|
|
|
|
|
|
"responseVersion": "回應版本",
|
|
|
|
|
|
"responseStatus": "回應狀態",
|
|
|
|
|
|
"responseReason": "回應原因",
|
|
|
|
|
|
"responseHeaders": "回應標頭",
|
|
|
|
|
|
"heartbeatPeriod": "心跳週期",
|
|
|
|
|
|
"serviceName": "服務名稱",
|
|
|
|
|
|
"authority": "Authority",
|
|
|
|
|
|
"multiMode": "多模式",
|
|
|
|
|
|
"maxBufferedUpload": "最大緩衝上傳",
|
|
|
|
|
|
"maxUploadSize": "最大上傳大小 (位元組)",
|
|
|
|
|
|
"streamUpServer": "Stream-Up 伺服器",
|
|
|
|
|
|
"serverMaxHeaderBytes": "伺服器最大標頭位元組",
|
|
|
|
|
|
"paddingBytes": "Padding 位元組",
|
|
|
|
|
|
"uplinkHttpMethod": "Uplink HTTP 方法",
|
|
|
|
|
|
"paddingObfsMode": "Padding 混淆模式",
|
|
|
|
|
|
"paddingKey": "Padding Key",
|
|
|
|
|
|
"paddingHeader": "Padding Header",
|
|
|
|
|
|
"paddingPlacement": "Padding 位置",
|
|
|
|
|
|
"paddingMethod": "Padding 方法",
|
|
|
|
|
|
"sessionPlacement": "Session 位置",
|
|
|
|
|
|
"sessionKey": "Session Key",
|
|
|
|
|
|
"sequencePlacement": "Sequence 位置",
|
|
|
|
|
|
"sequenceKey": "Sequence Key",
|
|
|
|
|
|
"uplinkDataPlacement": "Uplink 資料位置",
|
|
|
|
|
|
"uplinkDataKey": "Uplink 資料 Key",
|
|
|
|
|
|
"noSseHeader": "無 SSE 標頭",
|
|
|
|
|
|
"ttiMs": "TTI (ms)",
|
|
|
|
|
|
"uplinkMbps": "上行 (MB/s)",
|
|
|
|
|
|
"downlinkMbps": "下行 (MB/s)",
|
|
|
|
|
|
"cwndMultiplier": "CWND 倍數",
|
|
|
|
|
|
"maxSendingWindow": "最大發送視窗",
|
|
|
|
|
|
"externalProxy": "外部代理",
|
|
|
|
|
|
"sniPlaceholder": "SNI (預設為 host)",
|
|
|
|
|
|
"fingerprint": "指紋",
|
|
|
|
|
|
"defaultOption": "預設",
|
|
|
|
|
|
"routeMark": "Route Mark",
|
|
|
|
|
|
"tcpKeepAliveInterval": "TCP Keep Alive 間隔",
|
|
|
|
|
|
"tcpKeepAliveIdle": "TCP Keep Alive Idle",
|
|
|
|
|
|
"tcpMaxSeg": "TCP Max Seg",
|
|
|
|
|
|
"tcpUserTimeout": "TCP User Timeout",
|
|
|
|
|
|
"tcpWindowClamp": "TCP Window Clamp",
|
|
|
|
|
|
"tcpFastOpen": "TCP Fast Open",
|
|
|
|
|
|
"multipathTcp": "Multipath TCP",
|
|
|
|
|
|
"penetrate": "Penetrate",
|
|
|
|
|
|
"v6Only": "僅 V6",
|
|
|
|
|
|
"tcpCongestion": "TCP Congestion",
|
|
|
|
|
|
"dialerProxy": "Dialer Proxy",
|
|
|
|
|
|
"trustedXForwardedFor": "信任的 X-Forwarded-For",
|
|
|
|
|
|
"addressPortStrategy": "地址+連接埠策略",
|
|
|
|
|
|
"tryDelayMs": "嘗試延遲 (ms)",
|
|
|
|
|
|
"prioritizeIPv6": "IPv6 優先",
|
|
|
|
|
|
"interleave": "Interleave",
|
|
|
|
|
|
"maxConcurrentTry": "最大並發嘗試",
|
|
|
|
|
|
"customSockopt": "自訂 sockopt",
|
|
|
|
|
|
"addCustomOption": "新增自訂選項",
|
|
|
|
|
|
"serverNameIndication": "SNI",
|
|
|
|
|
|
"cipherSuites": "Cipher Suites",
|
|
|
|
|
|
"autoOption": "自動",
|
|
|
|
|
|
"minMaxVersion": "最小/最大版本",
|
|
|
|
|
|
"rejectUnknownSni": "拒絕未知 SNI",
|
|
|
|
|
|
"disableSystemRoot": "停用系統根",
|
|
|
|
|
|
"sessionResumption": "工作階段恢復",
|
|
|
|
|
|
"oneTimeLoading": "一次性載入",
|
|
|
|
|
|
"usageOption": "使用選項",
|
|
|
|
|
|
"buildChain": "建立憑證鏈",
|
|
|
|
|
|
"echKey": "ECH key",
|
|
|
|
|
|
"echConfig": "ECH 設定",
|
2026-05-28 17:32:10 +00:00
|
|
|
|
"pinnedPeerCertSha256": "釘選對端憑證 SHA-256",
|
|
|
|
|
|
"pinnedPeerCertSha256Tip": "對端憑證的 Base64 編碼 SHA-256 雜湊。僅面板使用 — 不寫入伺服器的 xray 設定,但會包含在分享連結中,以便用戶端釘選憑證。",
|
|
|
|
|
|
"pinnedPeerCertSha256Placeholder": "base64 雜湊,以逗號分隔",
|
|
|
|
|
|
"generateRandomPin": "產生隨機雜湊",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"getNewEchCert": "取得新 ECH 憑證",
|
|
|
|
|
|
"show": "顯示",
|
|
|
|
|
|
"xver": "Xver",
|
|
|
|
|
|
"target": "目標",
|
|
|
|
|
|
"maxTimeDiff": "最大時間差 (ms)",
|
|
|
|
|
|
"minClientVer": "最小客戶端版本",
|
|
|
|
|
|
"maxClientVer": "最大客戶端版本",
|
|
|
|
|
|
"shortIds": "Short IDs",
|
|
|
|
|
|
"spiderX": "SpiderX",
|
|
|
|
|
|
"getNewCert": "取得新憑證",
|
|
|
|
|
|
"mldsa65Seed": "mldsa65 Seed",
|
|
|
|
|
|
"mldsa65Verify": "mldsa65 Verify",
|
|
|
|
|
|
"getNewSeed": "取得新 Seed"
|
|
|
|
|
|
},
|
|
|
|
|
|
"info": {
|
|
|
|
|
|
"mode": "模式",
|
|
|
|
|
|
"grpcServiceName": "grpc serviceName",
|
|
|
|
|
|
"grpcMultiMode": "grpc multiMode",
|
|
|
|
|
|
"interfaceName": "介面名稱",
|
|
|
|
|
|
"mtu": "MTU",
|
|
|
|
|
|
"gateway": "Gateway",
|
|
|
|
|
|
"dns": "DNS",
|
|
|
|
|
|
"outboundsInterface": "出站介面",
|
|
|
|
|
|
"autoSystemRoutes": "自動系統路由",
|
|
|
|
|
|
"followRedirect": "FollowRedirect",
|
|
|
|
|
|
"auth": "認證",
|
|
|
|
|
|
"noKernelTun": "非核心 TUN",
|
|
|
|
|
|
"keepAlive": "Keep alive",
|
|
|
|
|
|
"peerNumber": "Peer {n}",
|
|
|
|
|
|
"peerNumberConfig": "Peer {n} 設定"
|
|
|
|
|
|
},
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"stream": {
|
|
|
|
|
|
"general": {
|
|
|
|
|
|
"request": "請求",
|
|
|
|
|
|
"response": "響應",
|
|
|
|
|
|
"name": "名稱",
|
|
|
|
|
|
"value": "值"
|
|
|
|
|
|
},
|
|
|
|
|
|
"tcp": {
|
|
|
|
|
|
"version": "版本",
|
|
|
|
|
|
"method": "方法",
|
|
|
|
|
|
"path": "路徑",
|
|
|
|
|
|
"status": "狀態",
|
|
|
|
|
|
"statusDescription": "狀態說明",
|
|
|
|
|
|
"requestHeader": "請求頭",
|
|
|
|
|
|
"responseHeader": "響應頭"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"clients": {
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"add": "新增客戶端",
|
|
|
|
|
|
"edit": "編輯客戶端",
|
|
|
|
|
|
"submitAdd": "新增客戶端",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"submitEdit": "儲存變更",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"clientCount": "客戶端數量",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"bulk": "批次新增",
|
|
|
|
|
|
"copyFromInbound": "從入站複製客戶端",
|
|
|
|
|
|
"copyToInbound": "複製客戶端至",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"copySelected": "複製所選",
|
|
|
|
|
|
"copySource": "來源",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"copyEmailPreview": "產生的信箱預覽",
|
|
|
|
|
|
"copySelectSourceFirst": "請先選擇一個來源入站。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"copyResult": "複製結果",
|
|
|
|
|
|
"copyResultSuccess": "複製成功",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"copyResultNone": "沒有內容可複製:未選取客戶端或來源為空",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"copyResultErrors": "複製錯誤",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"copyFlowLabel": "新客戶端的 Flow (VLESS)",
|
|
|
|
|
|
"copyFlowHint": "套用至所有被複製的客戶端。留空則略過。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"selectAll": "全選",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"clearAll": "全部清除",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"method": "方法",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"first": "首個",
|
|
|
|
|
|
"last": "末位",
|
|
|
|
|
|
"ipLog": "IP 日誌",
|
|
|
|
|
|
"prefix": "前綴",
|
|
|
|
|
|
"postfix": "後綴",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"delayedStart": "首次使用後開始",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"expireDays": "時長",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"days": "天",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"renew": "自動續期",
|
|
|
|
|
|
"renewDesc": "到期後自動續期。(0 = 停用) (單位: 天)",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"searchPlaceholder": "搜尋電子郵件、備註、sub ID、UUID、密碼、auth…",
|
|
|
|
|
|
"filterTitle": "篩選客戶端",
|
|
|
|
|
|
"clearAllFilters": "清除全部",
|
|
|
|
|
|
"sortOldest": "最舊優先",
|
|
|
|
|
|
"sortNewest": "最新優先",
|
|
|
|
|
|
"sortRecentlyUpdated": "最近更新",
|
|
|
|
|
|
"sortRecentlyOnline": "最近上線",
|
|
|
|
|
|
"sortEmailAZ": "電子郵件 A→Z",
|
|
|
|
|
|
"sortEmailZA": "電子郵件 Z→A",
|
|
|
|
|
|
"sortMostTraffic": "流量最多",
|
|
|
|
|
|
"sortHighestRemaining": "剩餘最多",
|
|
|
|
|
|
"sortExpiringSoonest": "即將到期",
|
|
|
|
|
|
"has": "擁有",
|
|
|
|
|
|
"hasNot": "不擁有",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"title": "客戶端",
|
|
|
|
|
|
"actions": "操作",
|
|
|
|
|
|
"totalGB": "總上傳/下載 (GB)",
|
|
|
|
|
|
"expiryTime": "到期時間",
|
|
|
|
|
|
"addClients": "新增客戶端",
|
|
|
|
|
|
"limitIp": "IP 限制",
|
|
|
|
|
|
"password": "密碼",
|
|
|
|
|
|
"subId": "訂閱 ID",
|
|
|
|
|
|
"online": "上線",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"email": "電子郵件",
|
2026-05-30 20:40:48 +00:00
|
|
|
|
"emailInvalidChars": "電子郵件不能包含空格、'/'、'\\' 或控制字元",
|
2026-05-30 21:28:58 +00:00
|
|
|
|
"subIdInvalidChars": "訂閱ID不能包含空格、'/'、'\\' 或控制字元",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"group": "群組",
|
|
|
|
|
|
"groupDesc": "用於將相關客戶端歸類的邏輯標籤(如團隊、客戶、地區)。可從工具列篩選。",
|
|
|
|
|
|
"groupPlaceholder": "如 customer-a",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"comment": "備註",
|
|
|
|
|
|
"traffic": "流量",
|
|
|
|
|
|
"offline": "離線",
|
2026-05-29 21:22:49 +00:00
|
|
|
|
"addClient": "新增客戶端",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"qrCode": "QR 碼",
|
2026-05-29 21:22:49 +00:00
|
|
|
|
"clientInfo": "客戶端資訊",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"delete": "刪除",
|
|
|
|
|
|
"reset": "重設流量",
|
2026-05-29 21:22:49 +00:00
|
|
|
|
"editClient": "編輯客戶端",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"client": "客戶端",
|
|
|
|
|
|
"enabled": "已啟用",
|
|
|
|
|
|
"remaining": "剩餘",
|
|
|
|
|
|
"duration": "時長",
|
|
|
|
|
|
"attachedInbounds": "關聯入站",
|
|
|
|
|
|
"selectInbound": "選擇一個或多個入站",
|
|
|
|
|
|
"noSubId": "此客戶端沒有 subId,無法產生共享連結。",
|
|
|
|
|
|
"noLinks": "沒有可共享的連結 — 請先將此客戶端關聯至支援協定的入站。",
|
|
|
|
|
|
"link": "連結",
|
|
|
|
|
|
"resetNotPossible": "請先將此客戶端關聯至入站。",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"general": "一般",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"resetAllTraffics": "重設所有客戶端流量",
|
|
|
|
|
|
"resetAllTrafficsTitle": "重設所有客戶端流量?",
|
|
|
|
|
|
"resetAllTrafficsContent": "所有客戶端的上下行計數器將歸零。配額與到期時間不受影響。此操作無法復原。",
|
|
|
|
|
|
"deleteConfirmTitle": "刪除客戶端 {email}?",
|
|
|
|
|
|
"deleteConfirmContent": "將從所有關聯入站中移除該客戶端並刪除其流量紀錄。此操作無法復原。",
|
|
|
|
|
|
"deleteSelected": "刪除 ({count})",
|
2026-05-23 14:27:20 +00:00
|
|
|
|
"adjustSelected": "調整 ({count})",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"subLinksSelected": "訂閱連結 ({count})",
|
|
|
|
|
|
"addToGroupTitle": "將 {count} 個客戶端加入群組",
|
|
|
|
|
|
"addToGroupTooltip": "選擇現有群組或輸入新名稱。使用 Ungroup 操作從當前群組移除客戶端。",
|
2026-05-29 21:22:49 +00:00
|
|
|
|
"groupName": "群組名稱",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"addToGroupSuccessToast": "已將 {count} 個客戶端加入 {group}",
|
|
|
|
|
|
"ungroupSuccessToast": "已清除 {count} 個客戶端的群組",
|
|
|
|
|
|
"ungroup": "取消群組",
|
|
|
|
|
|
"ungroupConfirmTitle": "將 {count} 個客戶端從其群組中移除?",
|
|
|
|
|
|
"ungroupConfirmContent": "清除每個選取客戶端的群組標籤。客戶端本身保留(用 Delete 完全移除)。",
|
|
|
|
|
|
"addToGroup": "加入群組",
|
|
|
|
|
|
"attach": "附加",
|
|
|
|
|
|
"adjust": "調整",
|
|
|
|
|
|
"subLinks": "訂閱連結",
|
|
|
|
|
|
"selectedCount": "已選 {count} 項",
|
|
|
|
|
|
"attachSelected": "附加 ({count})",
|
|
|
|
|
|
"attachToInboundsTitle": "將 {count} 個客戶端附加到入站",
|
|
|
|
|
|
"attachToInboundsDesc": "將選取的 {count} 個客戶端(相同 UUID/密碼與共享流量)附加到選定入站。它們保留現有附加關係。",
|
|
|
|
|
|
"attachToInboundsTargets": "目標入站",
|
|
|
|
|
|
"attachToInboundsNoTargets": "沒有可供附加的多用戶入站。",
|
|
|
|
|
|
"detachSelected": "分離 ({count})",
|
|
|
|
|
|
"detach": "分離",
|
|
|
|
|
|
"detachFromInboundsTitle": "從入站分離 {count} 個客戶端",
|
|
|
|
|
|
"detachFromInboundsDesc": "從選定入站中移除選取的 {count} 個客戶端。客戶端未附加的配對會被靜默略過。客戶端記錄保留(用 Delete 完全移除)。",
|
|
|
|
|
|
"detachFromInboundsTargets": "要分離的入站",
|
|
|
|
|
|
"detachFromInboundsNoTargets": "沒有可用的多用戶入站。",
|
|
|
|
|
|
"detachFromInboundsResult": "已分離 {detached},已略過 {skipped}。",
|
|
|
|
|
|
"detachFromInboundsResultMixed": "已分離 {detached},已略過 {skipped},錯誤 {errors}。",
|
|
|
|
|
|
"subLinksTitle": "訂閱連結 ({count})",
|
|
|
|
|
|
"subLinkColumn": "訂閱 URL",
|
|
|
|
|
|
"subJsonLinkColumn": "訂閱 JSON URL",
|
|
|
|
|
|
"subLinksCopyAll": "全部複製",
|
|
|
|
|
|
"subLinksCopiedAll": "已複製 {count} 條連結",
|
|
|
|
|
|
"subLinksEmpty": "選取的客戶端皆無訂閱 ID。",
|
|
|
|
|
|
"subLinksDisabled": "訂閱服務已停用。",
|
|
|
|
|
|
"subLinksDisabledHint": "在面板設定 → 訂閱中啟用訂閱以產生連結。",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"bulkDeleteConfirmTitle": "刪除 {count} 個客戶端?",
|
|
|
|
|
|
"bulkDeleteConfirmContent": "每個所選客戶端都會從關聯的入站中被移除,其流量紀錄也會被刪除。此操作無法復原。",
|
2026-05-23 14:27:20 +00:00
|
|
|
|
"bulkAdjustTitle": "調整 {count} 個客戶端",
|
|
|
|
|
|
"bulkAdjustHint": "正值延長,負值減少。具有無限期限或流量的客戶端將跳過該欄位。",
|
|
|
|
|
|
"bulkAdjustNothing": "套用前請設定天數或流量。",
|
|
|
|
|
|
"addDays": "新增天數",
|
|
|
|
|
|
"addTrafficGB": "新增流量 (GB)",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"delDepleted": "刪除已耗盡",
|
|
|
|
|
|
"delDepletedConfirmTitle": "刪除已耗盡的客戶端?",
|
|
|
|
|
|
"delDepletedConfirmContent": "刪除所有流量配額已用盡或已過期的客戶端。此操作無法復原。",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"auth": "認證",
|
|
|
|
|
|
"hysteriaAuth": "Hysteria 認證",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"uuid": "UUID",
|
|
|
|
|
|
"flow": "Flow",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"vmessSecurity": "VMess 加密",
|
|
|
|
|
|
"reverseTag": "反向標籤",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"reverseTagPlaceholder": "選用 Reverse tag",
|
|
|
|
|
|
"telegramId": "Telegram 使用者 ID",
|
|
|
|
|
|
"telegramIdPlaceholder": "數字形式的 Telegram 使用者 ID (0 = 無)",
|
|
|
|
|
|
"created": "建立時間",
|
|
|
|
|
|
"updated": "更新時間",
|
|
|
|
|
|
"ipLimit": "IP 限制",
|
|
|
|
|
|
"toasts": {
|
|
|
|
|
|
"deleted": "客戶端已刪除",
|
|
|
|
|
|
"trafficReset": "流量已重設",
|
|
|
|
|
|
"allTrafficsReset": "所有客戶端流量已重設",
|
|
|
|
|
|
"bulkDeleted": "已刪除 {count} 個客戶端",
|
|
|
|
|
|
"bulkDeletedMixed": "已刪除 {ok} 個,失敗 {failed} 個",
|
|
|
|
|
|
"bulkCreated": "已建立 {count} 個客戶端",
|
|
|
|
|
|
"bulkCreatedMixed": "已建立 {ok} 個,失敗 {failed} 個",
|
2026-05-23 14:27:20 +00:00
|
|
|
|
"bulkAdjusted": "已調整 {count} 個客戶端",
|
|
|
|
|
|
"bulkAdjustedMixed": "已調整 {ok} 個,跳過 {skipped} 個",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"delDepleted": "已刪除 {count} 個已耗盡的客戶端"
|
|
|
|
|
|
}
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
},
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"groups": {
|
|
|
|
|
|
"title": "群組",
|
|
|
|
|
|
"name": "名稱",
|
|
|
|
|
|
"clientCount": "群組中的客戶端",
|
|
|
|
|
|
"totalGroups": "群組總數",
|
|
|
|
|
|
"totalGroupedClients": "有群組的客戶端",
|
|
|
|
|
|
"emptyGroups": "空群組",
|
|
|
|
|
|
"addGroup": "新增群組",
|
|
|
|
|
|
"createSuccess": "已建立群組「{name}」。",
|
|
|
|
|
|
"rename": "重新命名",
|
|
|
|
|
|
"renameTitle": "重新命名 {name}",
|
|
|
|
|
|
"renameCollision": "已存在名為「{name}」的群組。",
|
|
|
|
|
|
"renameSuccess": "已為 {count} 個客戶端重新命名群組。",
|
|
|
|
|
|
"deleteConfirmTitle": "刪除群組 {name}?",
|
|
|
|
|
|
"deleteConfirmContent": "這將刪除群組並清除 {count} 個客戶端的標籤。客戶端本身不會被刪除。",
|
|
|
|
|
|
"deleteSuccess": "已清除 {count} 個客戶端的群組。",
|
|
|
|
|
|
"resetTraffic": "重置流量",
|
|
|
|
|
|
"resetConfirmTitle": "重置群組 {name} 的流量?",
|
|
|
|
|
|
"resetConfirmContent": "這將將此群組中所有 {count} 個客戶端的上行/下行流量歸零。",
|
|
|
|
|
|
"resetSuccess": "已重置 {count} 個客戶端的流量。",
|
|
|
|
|
|
"adjustSuccess": "已調整 {name} 中的 {count} 個客戶端。",
|
|
|
|
|
|
"emptyForAction": "此群組尚無客戶端。",
|
|
|
|
|
|
"deleteGroupOnly": "刪除群組(保留客戶端)",
|
|
|
|
|
|
"deleteClients": "刪除群組中的客戶端",
|
|
|
|
|
|
"deleteClientsConfirmTitle": "刪除 {name} 中的所有客戶端?",
|
|
|
|
|
|
"deleteClientsConfirmContent": "這將永久刪除 {count} 個客戶端及其流量記錄。群組標籤亦會被清除。此操作無法復原。",
|
|
|
|
|
|
"deleteClientsSuccess": "已刪除 {count} 個客戶端。",
|
|
|
|
|
|
"deleteClientsMixed": "已刪除 {ok},已略過 {failed}",
|
|
|
|
|
|
"addToGroup": "新增客戶端…",
|
|
|
|
|
|
"addToGroupTitle": "將客戶端加入群組「{name}」",
|
|
|
|
|
|
"addToGroupDesc": "選擇要加入此群組的客戶端。保留其現有入站附加;僅更改群組標籤。已在此群組中的客戶端不會列出。",
|
|
|
|
|
|
"addToGroupEmpty": "沒有其他可加入的客戶端。",
|
|
|
|
|
|
"addToGroupResult": "已將 {count} 個客戶端加入 {name}。",
|
|
|
|
|
|
"removeFromGroup": "移除客戶端…",
|
|
|
|
|
|
"removeFromGroupTitle": "從群組「{name}」移除客戶端",
|
|
|
|
|
|
"removeFromGroupDesc": "選擇要從此群組移除的成員。客戶端本身保留(用「刪除群組中的客戶端」完全移除)。",
|
|
|
|
|
|
"removeFromGroupResult": "已從 {name} 移除 {count} 個客戶端。"
|
|
|
|
|
|
},
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"nodes": {
|
|
|
|
|
|
"title": "節點",
|
|
|
|
|
|
"addNode": "新增節點",
|
|
|
|
|
|
"editNode": "編輯節點",
|
|
|
|
|
|
"totalNodes": "節點總數",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"onlineNodes": "上線",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"offlineNodes": "離線",
|
|
|
|
|
|
"avgLatency": "平均延遲",
|
|
|
|
|
|
"name": "名稱",
|
|
|
|
|
|
"namePlaceholder": "例如:de-frankfurt-1",
|
|
|
|
|
|
"addressPlaceholder": "panel.example.com 或 1.2.3.4",
|
|
|
|
|
|
"remark": "備註",
|
|
|
|
|
|
"scheme": "協議",
|
|
|
|
|
|
"address": "位址",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"port": "連接埠",
|
2026-05-31 14:17:06 +00:00
|
|
|
|
"basePath": "基礎路徑",
|
|
|
|
|
|
"apiToken": "API 權杖",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"apiTokenPlaceholder": "遠端面板設定頁中的權杖",
|
|
|
|
|
|
"apiTokenHint": "遠端面板在 設定 → API 權杖 中顯示其 API 權杖。",
|
|
|
|
|
|
"regenerate": "重新產生權杖",
|
|
|
|
|
|
"regenerateConfirm": "重新產生會使目前的權杖失效。任何使用該權杖的中央面板將失去存取權,直到更新為止。是否繼續?",
|
2026-05-15 07:51:14 +00:00
|
|
|
|
"allowPrivateAddress": "允許私有地址",
|
|
|
|
|
|
"allowPrivateAddressHint": "僅對私有網路或VPN上的節點啟用。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"enable": "已啟用",
|
|
|
|
|
|
"status": "狀態",
|
|
|
|
|
|
"cpu": "CPU",
|
|
|
|
|
|
"mem": "記憶體",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"uptime": "運行時間",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"latency": "延遲",
|
|
|
|
|
|
"lastHeartbeat": "上次心跳",
|
|
|
|
|
|
"xrayVersion": "Xray 版本",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"panelVersion": "面板版本",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"actions": "操作",
|
|
|
|
|
|
"probe": "立即探測",
|
|
|
|
|
|
"testConnection": "測試連線",
|
|
|
|
|
|
"connectionOk": "連線正常 ({ms} ms)",
|
|
|
|
|
|
"connectionFailed": "連線失敗",
|
|
|
|
|
|
"never": "從未",
|
|
|
|
|
|
"justNow": "剛剛",
|
|
|
|
|
|
"deleteConfirmTitle": "刪除節點「{name}」?",
|
|
|
|
|
|
"deleteConfirmContent": "這將停止監控該節點。遠端面板本身不受影響。",
|
|
|
|
|
|
"statusValues": {
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"online": "上線",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"offline": "離線",
|
|
|
|
|
|
"unknown": "未知"
|
|
|
|
|
|
},
|
|
|
|
|
|
"toasts": {
|
|
|
|
|
|
"list": "載入節點失敗",
|
|
|
|
|
|
"obtain": "載入節點失敗",
|
|
|
|
|
|
"add": "新增節點",
|
|
|
|
|
|
"update": "更新節點",
|
|
|
|
|
|
"delete": "刪除節點",
|
|
|
|
|
|
"deleted": "節點已刪除",
|
|
|
|
|
|
"test": "測試連線",
|
|
|
|
|
|
"fillRequired": "名稱、位址、埠與 API 權杖為必填",
|
|
|
|
|
|
"probeFailed": "探測失敗"
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
"settings": {
|
|
|
|
|
|
"title": "面板設定",
|
|
|
|
|
|
"save": "儲存",
|
|
|
|
|
|
"infoDesc": "此處的所有更改都需要儲存並重啟面板才能生效",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"restartPanel": "重新啟動面板",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"restartPanelDesc": "確定要重啟面板嗎?若重啟後無法訪問面板,請前往伺服器檢視面板日誌資訊",
|
|
|
|
|
|
"restartPanelSuccess": "面板已成功重新啟動",
|
|
|
|
|
|
"actions": "操作",
|
|
|
|
|
|
"resetDefaultConfig": "重置為預設配置",
|
|
|
|
|
|
"panelSettings": "常規",
|
|
|
|
|
|
"securitySettings": "安全設定",
|
2026-05-23 17:43:21 +00:00
|
|
|
|
"securityWarnings": "安全警告",
|
|
|
|
|
|
"panelExposed": "您的面板可能已暴露:",
|
|
|
|
|
|
"warnHttp": "面板透過明文 HTTP 提供服務 — 生產環境請設定 TLS。",
|
|
|
|
|
|
"warnDefaultPort": "預設連接埠 2053 廣為人知 — 請更改為隨機連接埠。",
|
|
|
|
|
|
"warnDefaultBasePath": "預設根路徑 \"/\" 廣為人知 — 請更改為隨機路徑。",
|
|
|
|
|
|
"warnDefaultSubPath": "預設訂閱路徑 \"/sub/\" 廣為人知 — 請更改。",
|
|
|
|
|
|
"warnDefaultJsonPath": "預設 JSON 訂閱路徑 \"/json/\" 廣為人知 — 請更改。",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"TGBotSettings": "Telegram 機器人",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"panelListeningIP": "面板監聽 IP",
|
|
|
|
|
|
"panelListeningIPDesc": "預設留空監聽所有 IP",
|
|
|
|
|
|
"panelListeningDomain": "面板監聽域名",
|
|
|
|
|
|
"panelListeningDomainDesc": "預設情況下留空以監視所有域名和 IP 地址",
|
|
|
|
|
|
"panelPort": "面板監聽埠",
|
|
|
|
|
|
"panelPortDesc": "重啟面板生效",
|
|
|
|
|
|
"publicKeyPath": "面板證書公鑰檔案路徑",
|
|
|
|
|
|
"publicKeyPathDesc": "填寫一個 '/' 開頭的絕對路徑",
|
|
|
|
|
|
"privateKeyPath": "面板證書金鑰檔案路徑",
|
|
|
|
|
|
"privateKeyPathDesc": "填寫一個 '/' 開頭的絕對路徑",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"panelUrlPath": "URI 路徑",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"panelUrlPathDesc": "必須以 '/' 開頭,以 '/' 結尾",
|
|
|
|
|
|
"pageSize": "分頁大小",
|
|
|
|
|
|
"pageSizeDesc": "定義入站表的頁面大小。設定 0 表示禁用",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"panelProxy": "面板網路代理",
|
|
|
|
|
|
"panelProxyDesc": "透過此代理路由面板自身的出站請求(geo 更新、Xray/面板版本檢查、Telegram),以繞過伺服器端對 GitHub/Telegram 的過濾。接受 socks5:// 或 http(s)://,如本地 Xray SOCKS 入站。留空表示直連。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"remarkModel": "備註模型和分隔符",
|
|
|
|
|
|
"datepicker": "日期選擇器",
|
|
|
|
|
|
"datepickerPlaceholder": "選擇日期",
|
|
|
|
|
|
"datepickerDescription": "選擇器日曆類型指定到期日期",
|
|
|
|
|
|
"sampleRemark": "備註示例",
|
|
|
|
|
|
"oldUsername": "原使用者名稱",
|
|
|
|
|
|
"currentPassword": "原密碼",
|
|
|
|
|
|
"newUsername": "新使用者名稱",
|
|
|
|
|
|
"newPassword": "新密碼",
|
|
|
|
|
|
"telegramBotEnable": "啟用 Telegram 機器人",
|
|
|
|
|
|
"telegramBotEnableDesc": "啟用 Telegram 機器人功能",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"telegramToken": "Telegram Token",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"telegramTokenDesc": "從 '{'@'}BotFather' 獲取的 Telegram 機器人令牌",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"telegramProxy": "SOCKS 代理",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"telegramProxyDesc": "啟用 SOCKS5 代理連線到 Telegram(根據指南調整設定)",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"telegramAPIServer": "Telegram API 伺服器",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"telegramAPIServerDesc": "要使用的 Telegram API 伺服器。留空以使用預設伺服器。",
|
|
|
|
|
|
"telegramChatId": "管理員聊天 ID",
|
|
|
|
|
|
"telegramChatIdDesc": "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 {'@'}userinfobot 獲取,或在機器人中使用 '/id' 命令獲取)",
|
|
|
|
|
|
"telegramNotifyTime": "通知時間",
|
|
|
|
|
|
"telegramNotifyTimeDesc": "設定週期性的 Telegram 機器人通知時間(使用 crontab 時間格式)",
|
|
|
|
|
|
"tgNotifyBackup": "資料庫備份",
|
|
|
|
|
|
"tgNotifyBackupDesc": "傳送帶有報告的資料庫備份檔案",
|
|
|
|
|
|
"tgNotifyLogin": "登入通知",
|
|
|
|
|
|
"tgNotifyLoginDesc": "當有人試圖登入你的面板時顯示使用者名稱、IP 地址和時間",
|
|
|
|
|
|
"sessionMaxAge": "會話時長",
|
|
|
|
|
|
"sessionMaxAgeDesc": "保持登入狀態的時長(單位:分鐘)",
|
|
|
|
|
|
"expireTimeDiff": "到期通知閾值",
|
|
|
|
|
|
"expireTimeDiffDesc": "達到此閾值時,將收到有關到期時間的通知(單位:天)",
|
|
|
|
|
|
"trafficDiff": "流量耗盡閾值",
|
|
|
|
|
|
"trafficDiffDesc": "達到此閾值時,將收到有關流量耗盡的通知(單位:GB)",
|
|
|
|
|
|
"tgNotifyCpu": "CPU 負載通知閾值",
|
|
|
|
|
|
"tgNotifyCpuDesc": "CPU 負載超過此閾值時,將收到通知(單位:%)",
|
|
|
|
|
|
"timeZone": "時區",
|
|
|
|
|
|
"timeZoneDesc": "定時任務將按照該時區的時間執行",
|
|
|
|
|
|
"subSettings": "訂閱設定",
|
|
|
|
|
|
"subEnable": "啟用訂閱服務",
|
|
|
|
|
|
"subEnableDesc": "啟用訂閱服務功能",
|
|
|
|
|
|
"subJsonEnable": "獨立啟用/停用 JSON 訂閱端點。",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"subJsonEnableTitle": "JSON 訂閱",
|
|
|
|
|
|
"subClashEnableTitle": "Clash / Mihomo 訂閱",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"subTitle": "訂閱標題",
|
|
|
|
|
|
"subTitleDesc": "在VPN客戶端中顯示的標題",
|
|
|
|
|
|
"subSupportUrl": "支援連結",
|
|
|
|
|
|
"subSupportUrlDesc": "VPN 用戶端中顯示的技術支援連結",
|
|
|
|
|
|
"subProfileUrl": "個人資料連結",
|
|
|
|
|
|
"subProfileUrlDesc": "VPN 用戶端中顯示的網站連結",
|
|
|
|
|
|
"subAnnounce": "公告",
|
|
|
|
|
|
"subAnnounceDesc": "VPN 用戶端中顯示的公告文字",
|
|
|
|
|
|
"subEnableRouting": "啟用路由",
|
|
|
|
|
|
"subEnableRoutingDesc": "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ)",
|
|
|
|
|
|
"subRoutingRules": "路由規則",
|
|
|
|
|
|
"subRoutingRulesDesc": "VPN 用戶端的全域路由規則。(僅限 Happ)",
|
|
|
|
|
|
"subListen": "監聽 IP",
|
|
|
|
|
|
"subListenDesc": "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)",
|
|
|
|
|
|
"subPort": "監聽埠",
|
|
|
|
|
|
"subPortDesc": "訂閱服務監聽的埠號(必須是未使用的埠)",
|
|
|
|
|
|
"subCertPath": "公鑰路徑",
|
|
|
|
|
|
"subCertPathDesc": "訂閱服務使用的公鑰檔案路徑(以 '/' 開頭)",
|
|
|
|
|
|
"subKeyPath": "私鑰路徑",
|
|
|
|
|
|
"subKeyPathDesc": "訂閱服務使用的私鑰檔案路徑(以 '/' 開頭)",
|
|
|
|
|
|
"subPath": "URI 路徑",
|
|
|
|
|
|
"subPathDesc": "訂閱服務使用的 URI 路徑(以 '/' 開頭,以 '/' 結尾)",
|
|
|
|
|
|
"subDomain": "監聽域名",
|
|
|
|
|
|
"subDomainDesc": "訂閱服務監聽的域名(留空表示監聽所有域名和 IP)",
|
|
|
|
|
|
"subUpdates": "更新間隔",
|
|
|
|
|
|
"subUpdatesDesc": "客戶端應用中訂閱 URL 的更新間隔(單位:小時)",
|
|
|
|
|
|
"subEncrypt": "編碼",
|
|
|
|
|
|
"subEncryptDesc": "訂閱服務返回的內容將採用 Base64 編碼",
|
|
|
|
|
|
"subShowInfo": "顯示使用資訊",
|
|
|
|
|
|
"subShowInfoDesc": "客戶端應用中將顯示剩餘流量和日期資訊",
|
2026-05-13 17:04:17 +00:00
|
|
|
|
"subEmailInRemark": "在名稱中包含郵箱",
|
|
|
|
|
|
"subEmailInRemarkDesc": "在訂閱配置名稱中包含客戶端郵箱。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"subURI": "反向代理 URI",
|
|
|
|
|
|
"subURIDesc": "用於代理後面的訂閱 URL 的 URI 路徑",
|
|
|
|
|
|
"externalTrafficInformEnable": "外部交通通知",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"externalTrafficInformEnableDesc": "每次流量更新時通知外部 API。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"externalTrafficInformURI": "外部流量通知 URI",
|
|
|
|
|
|
"externalTrafficInformURIDesc": "流量更新將會傳送到此 URI",
|
|
|
|
|
|
"restartXrayOnClientDisable": "用戶自動停用後重新啟動 Xray",
|
|
|
|
|
|
"restartXrayOnClientDisableDesc": "當用戶因到期或流量上限而被自動停用時,重新啟動 Xray。",
|
|
|
|
|
|
"fragment": "分片",
|
|
|
|
|
|
"fragmentDesc": "啟用 TLS hello 資料包分片",
|
|
|
|
|
|
"fragmentSett": "設定",
|
|
|
|
|
|
"noisesDesc": "啟用 Noises.",
|
|
|
|
|
|
"noisesSett": "Noises 設定",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"trustedProxyCidrs": "信任代理 CIDR",
|
|
|
|
|
|
"trustedProxyCidrsDesc": "允許設定轉發 host、proto 與客戶端 IP 標頭的 IP/CIDR(逗號分隔)。",
|
|
|
|
|
|
"ldap": {
|
|
|
|
|
|
"enable": "啟用 LDAP 同步",
|
|
|
|
|
|
"host": "LDAP host",
|
|
|
|
|
|
"port": "LDAP 連接埠",
|
|
|
|
|
|
"useTls": "使用 TLS (LDAPS)",
|
|
|
|
|
|
"bindDn": "Bind DN",
|
|
|
|
|
|
"passwordConfigured": "已設定;留空以保留目前密碼。",
|
|
|
|
|
|
"passwordUnconfigured": "未設定。",
|
|
|
|
|
|
"passwordPlaceholder": "已設定 - 輸入新值以取代",
|
|
|
|
|
|
"baseDn": "Base DN",
|
|
|
|
|
|
"userFilter": "使用者篩選",
|
|
|
|
|
|
"userAttr": "使用者屬性 (username/email)",
|
|
|
|
|
|
"vlessField": "VLESS flag 屬性",
|
|
|
|
|
|
"flagField": "通用 flag 屬性 (選用)",
|
|
|
|
|
|
"flagFieldDesc": "若設定,將覆寫 VLESS flag — 如 shadowInactive。",
|
|
|
|
|
|
"truthyValues": "Truthy 值",
|
|
|
|
|
|
"truthyValuesDesc": "以逗號分隔;預設: true,1,yes,on",
|
|
|
|
|
|
"invertFlag": "反轉 flag",
|
|
|
|
|
|
"invertFlagDesc": "當屬性表示已停用時啟用 (如 shadowInactive)。",
|
|
|
|
|
|
"syncSchedule": "同步排程",
|
|
|
|
|
|
"syncScheduleDesc": "類 cron 字串,如 @every 1m",
|
|
|
|
|
|
"inboundTags": "入站標籤",
|
|
|
|
|
|
"inboundTagsDesc": "允許 LDAP 同步自動建立或刪除客戶端的入站。",
|
|
|
|
|
|
"noInbounds": "未找到入站。請先在「入站」中建立。",
|
|
|
|
|
|
"autoCreate": "自動建立客戶端",
|
|
|
|
|
|
"autoDelete": "自動刪除客戶端",
|
|
|
|
|
|
"defaultTotalGb": "預設總流量 (GB)",
|
|
|
|
|
|
"defaultExpiryDays": "預設到期 (天)",
|
|
|
|
|
|
"defaultIpLimit": "預設 IP 限制"
|
|
|
|
|
|
},
|
|
|
|
|
|
"subFormats": {
|
|
|
|
|
|
"packets": "封包",
|
|
|
|
|
|
"length": "長度",
|
|
|
|
|
|
"interval": "間隔",
|
|
|
|
|
|
"maxSplit": "最大分割",
|
|
|
|
|
|
"noises": "雜訊",
|
|
|
|
|
|
"noiseItem": "雜訊 №{n}",
|
|
|
|
|
|
"type": "類型",
|
|
|
|
|
|
"packet": "封包",
|
|
|
|
|
|
"delayMs": "延遲 (ms)",
|
|
|
|
|
|
"applyTo": "套用至",
|
|
|
|
|
|
"addNoise": "+ 雜訊",
|
|
|
|
|
|
"concurrency": "並發",
|
|
|
|
|
|
"xudpConcurrency": "xudp 並發",
|
|
|
|
|
|
"xudpUdp443": "xudp UDP 443"
|
|
|
|
|
|
},
|
|
|
|
|
|
"mux": "Mux",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"muxDesc": "在已建立的資料流內傳輸多個獨立的資料流",
|
|
|
|
|
|
"muxSett": "複用器設定",
|
|
|
|
|
|
"direct": "直接連線",
|
|
|
|
|
|
"directDesc": "直接與特定國家的域或IP範圍建立連線",
|
|
|
|
|
|
"notifications": "通知",
|
|
|
|
|
|
"certs": "證書",
|
|
|
|
|
|
"externalTraffic": "外部流量",
|
|
|
|
|
|
"dateAndTime": "日期和時間",
|
|
|
|
|
|
"proxyAndServer": "代理和伺服器",
|
|
|
|
|
|
"intervals": "間隔",
|
|
|
|
|
|
"information": "資訊",
|
|
|
|
|
|
"language": "語言",
|
|
|
|
|
|
"telegramBotLanguage": "Telegram 機器人語言",
|
|
|
|
|
|
"security": {
|
|
|
|
|
|
"admin": "管理員憑證",
|
|
|
|
|
|
"twoFactor": "雙重驗證",
|
|
|
|
|
|
"twoFactorEnable": "啟用2FA",
|
|
|
|
|
|
"twoFactorEnableDesc": "增加額外的驗證層以提高安全性。",
|
|
|
|
|
|
"twoFactorModalSetTitle": "啟用雙重認證",
|
|
|
|
|
|
"twoFactorModalDeleteTitle": "停用雙重認證",
|
|
|
|
|
|
"twoFactorModalSteps": "要設定雙重認證,請執行以下步驟:",
|
|
|
|
|
|
"twoFactorModalFirstStep": "1. 在認證應用程式中掃描此QR碼,或複製QR碼附近的令牌並貼到應用程式中",
|
|
|
|
|
|
"twoFactorModalSecondStep": "2. 輸入應用程式中的驗證碼",
|
|
|
|
|
|
"twoFactorModalRemoveStep": "輸入應用程式中的驗證碼以移除雙重認證。",
|
|
|
|
|
|
"twoFactorModalChangeCredentialsTitle": "更改憑證",
|
|
|
|
|
|
"twoFactorModalChangeCredentialsStep": "輸入應用程式中的代碼以更改管理員憑證。",
|
|
|
|
|
|
"twoFactorModalSetSuccess": "雙重身份驗證已成功建立",
|
|
|
|
|
|
"twoFactorModalDeleteSuccess": "雙重身份驗證已成功刪除",
|
2026-05-13 14:34:31 +00:00
|
|
|
|
"twoFactorModalError": "驗證碼錯誤",
|
|
|
|
|
|
"show": "顯示",
|
|
|
|
|
|
"hide": "隱藏",
|
|
|
|
|
|
"apiTokenNew": "新增令牌",
|
|
|
|
|
|
"apiTokenName": "名稱",
|
|
|
|
|
|
"apiTokenNamePlaceholder": "例如 central-panel-a",
|
|
|
|
|
|
"apiTokenNameRequired": "名稱必填",
|
|
|
|
|
|
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
|
|
|
|
|
|
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。"
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
},
|
|
|
|
|
|
"toasts": {
|
|
|
|
|
|
"modifySettings": "參數已更改。",
|
|
|
|
|
|
"getSettings": "取得參數時發生錯誤",
|
|
|
|
|
|
"modifyUserError": "變更管理員憑證時發生錯誤。",
|
|
|
|
|
|
"modifyUser": "您已成功變更管理員憑證。",
|
|
|
|
|
|
"originalUserPassIncorrect": "原使用者名稱或原密碼錯誤",
|
|
|
|
|
|
"userPassMustBeNotEmpty": "新使用者名稱和新密碼不能為空",
|
|
|
|
|
|
"getOutboundTrafficError": "取得出站流量錯誤",
|
|
|
|
|
|
"resetOutboundTrafficError": "重設出站流量錯誤"
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
"xray": {
|
|
|
|
|
|
"title": "Xray 配置",
|
|
|
|
|
|
"save": "儲存",
|
|
|
|
|
|
"restart": "重新啟動 Xray",
|
|
|
|
|
|
"restartSuccess": "Xray 已成功重新啟動",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"restartOutputTitle": "Xray 重新啟動輸出",
|
|
|
|
|
|
"restartConfirmTitle": "重新啟動 xray?",
|
|
|
|
|
|
"restartConfirmContent": "使用已儲存的設定重新載入 xray 服務。",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"stopSuccess": "Xray 已成功停止",
|
|
|
|
|
|
"restartError": "重新啟動Xray時發生錯誤。",
|
|
|
|
|
|
"stopError": "停止Xray時發生錯誤。",
|
|
|
|
|
|
"basicTemplate": "基礎配置",
|
|
|
|
|
|
"advancedTemplate": "高階配置",
|
|
|
|
|
|
"generalConfigs": "常規配置",
|
|
|
|
|
|
"generalConfigsDesc": "這些選項將決定常規配置",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"logConfigs": "記錄",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"logConfigsDesc": "日誌可能會影響伺服器的效能,建議僅在需要時啟用",
|
|
|
|
|
|
"blockConfigsDesc": "這些選項將阻止使用者連線到特定協議和網站",
|
|
|
|
|
|
"basicRouting": "基本路由",
|
|
|
|
|
|
"blockConnectionsConfigsDesc": "這些選項將根據特定的請求國家阻止流量。",
|
|
|
|
|
|
"directConnectionsConfigsDesc": "直接連線確保特定的流量不會通過其他伺服器路由。",
|
|
|
|
|
|
"blockips": "阻止IP",
|
|
|
|
|
|
"blockdomains": "阻止域名",
|
|
|
|
|
|
"directips": "直接IP",
|
|
|
|
|
|
"directdomains": "直接域名",
|
|
|
|
|
|
"ipv4Routing": "IPv4 路由",
|
|
|
|
|
|
"ipv4RoutingDesc": "此選項將僅通過 IPv4 路由到目標域",
|
|
|
|
|
|
"warpRouting": "WARP 路由",
|
|
|
|
|
|
"warpRoutingDesc": "注意:在使用這些選項之前,請按照面板 GitHub 上的步驟在你的伺服器上以 socks5 代理模式安裝 WARP。WARP 將通過 Cloudflare 伺服器將流量路由到網站。",
|
|
|
|
|
|
"nordRouting": "NordVPN 路由",
|
|
|
|
|
|
"nordRoutingDesc": "這些選項將根據特定目的地通過 NordVPN 路由流量。",
|
|
|
|
|
|
"Template": "高階 Xray 配置模板",
|
|
|
|
|
|
"TemplateDesc": "最終的 Xray 配置檔案將基於此模板生成",
|
|
|
|
|
|
"FreedomStrategy": "Freedom 協議策略",
|
|
|
|
|
|
"FreedomStrategyDesc": "設定 Freedom 協議中網路的輸出策略",
|
|
|
|
|
|
"RoutingStrategy": "配置路由域策略",
|
|
|
|
|
|
"RoutingStrategyDesc": "設定 DNS 解析的整體路由策略",
|
|
|
|
|
|
"outboundTestUrl": "出站測試 URL",
|
|
|
|
|
|
"outboundTestUrlDesc": "測試出站連線時使用的 URL",
|
|
|
|
|
|
"Torrent": "遮蔽 BitTorrent 協議",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"Inbounds": "入站",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"InboundsDesc": "接受來自特定客戶端的流量",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"Outbounds": "出站",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"Balancers": "負載均衡",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"balancerTagRequired": "標籤為必填",
|
|
|
|
|
|
"balancerSelectorRequired": "至少選擇一個出站",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"OutboundsDesc": "設定出站流量傳出方式",
|
|
|
|
|
|
"Routings": "路由規則",
|
|
|
|
|
|
"RoutingsDesc": "每條規則的優先順序都很重要",
|
|
|
|
|
|
"completeTemplate": "全部",
|
|
|
|
|
|
"logLevel": "日誌級別",
|
|
|
|
|
|
"logLevelDesc": "錯誤日誌的日誌級別,用於指示需要記錄的資訊",
|
|
|
|
|
|
"accessLog": "訪問日誌",
|
|
|
|
|
|
"accessLogDesc": "訪問日誌的檔案路徑。特殊值 'none' 禁用訪問日誌",
|
|
|
|
|
|
"errorLog": "錯誤日誌",
|
|
|
|
|
|
"errorLogDesc": "錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌",
|
|
|
|
|
|
"dnsLog": "DNS 日誌",
|
|
|
|
|
|
"dnsLogDesc": "是否啟用 DNS 查詢日誌",
|
|
|
|
|
|
"maskAddress": "隱藏地址",
|
|
|
|
|
|
"maskAddressDesc": "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。",
|
|
|
|
|
|
"statistics": "統計",
|
|
|
|
|
|
"statsInboundUplink": "入站上傳統計",
|
|
|
|
|
|
"statsInboundUplinkDesc": "啟用所有入站代理的上行流量統計收集。",
|
|
|
|
|
|
"statsInboundDownlink": "入站下載統計",
|
|
|
|
|
|
"statsInboundDownlinkDesc": "啟用所有入站代理的下行流量統計收集。",
|
|
|
|
|
|
"statsOutboundUplink": "出站上傳統計",
|
|
|
|
|
|
"statsOutboundUplinkDesc": "啟用所有出站代理的上行流量統計收集。",
|
|
|
|
|
|
"statsOutboundDownlink": "出站下載統計",
|
|
|
|
|
|
"statsOutboundDownlinkDesc": "啟用所有出站代理的下行流量統計收集。",
|
|
|
|
|
|
"rules": {
|
|
|
|
|
|
"first": "置頂",
|
|
|
|
|
|
"last": "置底",
|
|
|
|
|
|
"up": "向上",
|
|
|
|
|
|
"down": "向下",
|
|
|
|
|
|
"source": "來源",
|
|
|
|
|
|
"dest": "目的地址",
|
|
|
|
|
|
"inbound": "入站",
|
|
|
|
|
|
"outbound": "出站",
|
|
|
|
|
|
"balancer": "負載均衡",
|
|
|
|
|
|
"info": "資訊",
|
|
|
|
|
|
"add": "新增規則",
|
|
|
|
|
|
"edit": "編輯規則",
|
|
|
|
|
|
"useComma": "逗號分隔的項目"
|
|
|
|
|
|
},
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"routing": {
|
|
|
|
|
|
"dragToReorder": "拖曳以重新排序"
|
|
|
|
|
|
},
|
|
|
|
|
|
"ruleForm": {
|
|
|
|
|
|
"sourceIps": "來源 IP",
|
|
|
|
|
|
"sourcePort": "來源連接埠",
|
|
|
|
|
|
"vlessRoute": "VLESS 路由",
|
|
|
|
|
|
"attributes": "屬性",
|
|
|
|
|
|
"value": "值",
|
|
|
|
|
|
"user": "使用者",
|
|
|
|
|
|
"inboundTags": "入站標籤",
|
|
|
|
|
|
"outboundTag": "出站標籤",
|
|
|
|
|
|
"balancerTag": "均衡器標籤",
|
|
|
|
|
|
"balancerTagTooltip": "透過其中一個已設定的負載均衡器路由流量"
|
|
|
|
|
|
},
|
|
|
|
|
|
"outboundForm": {
|
|
|
|
|
|
"tagDuplicate": "該標籤已被其他出站使用",
|
|
|
|
|
|
"tagRequired": "標籤為必填",
|
|
|
|
|
|
"tagPlaceholder": "唯一標籤",
|
|
|
|
|
|
"localIpPlaceholder": "本地 IP",
|
|
|
|
|
|
"addressRequired": "地址為必填",
|
|
|
|
|
|
"portRequired": "連接埠為必填",
|
|
|
|
|
|
"optional": "選用",
|
|
|
|
|
|
"udpOverTcp": "UDP over TCP",
|
|
|
|
|
|
"uotVersion": "UoT 版本",
|
|
|
|
|
|
"inboundTag": "入站標籤",
|
|
|
|
|
|
"inboundTagPlaceholder": "用於路由規則的入站標籤",
|
|
|
|
|
|
"responseType": "回應類型",
|
|
|
|
|
|
"rewriteNetwork": "改寫網路",
|
|
|
|
|
|
"unchanged": "(未變更)",
|
|
|
|
|
|
"unchangedAddress": "(未變更) 如 1.1.1.1",
|
|
|
|
|
|
"rules": "規則",
|
|
|
|
|
|
"ruleN": "規則 {n}",
|
|
|
|
|
|
"action": "動作",
|
|
|
|
|
|
"redirect": "Redirect",
|
|
|
|
|
|
"fragment": "Fragment",
|
|
|
|
|
|
"finalRules": "最終規則",
|
|
|
|
|
|
"overrideXrayPrivateIp": "覆寫 Xray 預設的私有 IP 封鎖",
|
|
|
|
|
|
"blockDelay": "阻斷延遲 (ms)",
|
|
|
|
|
|
"reverseSniffing": "反向 sniffing",
|
|
|
|
|
|
"workers": "Workers",
|
|
|
|
|
|
"reserved": "保留",
|
|
|
|
|
|
"minUploadInterval": "最小上傳間隔 (ms)",
|
|
|
|
|
|
"maxUploadSizeBytes": "最大上傳大小 (位元組)",
|
|
|
|
|
|
"uplinkChunkSize": "Uplink chunk 大小",
|
|
|
|
|
|
"noGrpcHeader": "無 gRPC 標頭",
|
|
|
|
|
|
"maxConcurrency": "最大並發",
|
|
|
|
|
|
"maxConnections": "最大連線",
|
|
|
|
|
|
"maxReuseTimes": "最大重用次數",
|
|
|
|
|
|
"maxRequestTimes": "最大請求次數",
|
|
|
|
|
|
"maxReusableSecs": "最大可重用秒數",
|
|
|
|
|
|
"keepAlivePeriod": "keep alive 週期",
|
|
|
|
|
|
"authPassword": "認證密碼",
|
|
|
|
|
|
"visionTestpre": "Vision testpre",
|
|
|
|
|
|
"serverNamePlaceholder": "伺服器名稱",
|
|
|
|
|
|
"verifyPeerName": "驗證 peer 名稱",
|
|
|
|
|
|
"pinnedSha256": "Pinned SHA256",
|
|
|
|
|
|
"shortId": "Short ID",
|
|
|
|
|
|
"sockopts": "Sockopts",
|
|
|
|
|
|
"keepAliveInterval": "keep alive 間隔",
|
|
|
|
|
|
"markFwmark": "Mark (fwmark)",
|
|
|
|
|
|
"interface": "介面",
|
|
|
|
|
|
"ipv6Only": "僅 IPv6",
|
|
|
|
|
|
"acceptProxyProtocol": "接受 proxy protocol",
|
2026-05-29 15:18:21 +00:00
|
|
|
|
"proxyProtocol": "Proxy protocol",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
|
|
|
|
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
|
|
|
|
|
},
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"outbound": {
|
|
|
|
|
|
"addOutbound": "新增出站",
|
|
|
|
|
|
"addReverse": "新增反向",
|
|
|
|
|
|
"editOutbound": "編輯出站",
|
|
|
|
|
|
"editReverse": "編輯反向",
|
|
|
|
|
|
"reverseTag": "反向標籤",
|
|
|
|
|
|
"reverseTagDesc": "VLESS 簡易反向代理出站標籤。留空則停用。設定後,此客戶端的連線可作為反向代理隧道。",
|
|
|
|
|
|
"reverseTagPlaceholder": "出站標籤(留空則停用)",
|
|
|
|
|
|
"tag": "標籤",
|
|
|
|
|
|
"tagDesc": "唯一標籤",
|
|
|
|
|
|
"address": "地址",
|
|
|
|
|
|
"reverse": "反向",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"domain": "網域",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"type": "類型",
|
|
|
|
|
|
"bridge": "Bridge",
|
|
|
|
|
|
"portal": "Portal",
|
|
|
|
|
|
"link": "連結",
|
|
|
|
|
|
"intercon": "互連",
|
|
|
|
|
|
"settings": "設定",
|
|
|
|
|
|
"accountInfo": "帳戶資訊",
|
|
|
|
|
|
"outboundStatus": "出站狀態",
|
|
|
|
|
|
"sendThrough": "傳送通過",
|
|
|
|
|
|
"test": "測試",
|
|
|
|
|
|
"testResult": "測試結果",
|
|
|
|
|
|
"testing": "正在測試連接...",
|
|
|
|
|
|
"testSuccess": "測試成功",
|
|
|
|
|
|
"testFailed": "測試失敗",
|
|
|
|
|
|
"testError": "測試出站失敗",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"testModeTooltip": "TCP: 快速 dial-only 探測。HTTP: 透過 xray 的完整請求。",
|
|
|
|
|
|
"testAll": "全部測試",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"nordvpn": "NordVPN",
|
|
|
|
|
|
"accessToken": "訪問令牌",
|
|
|
|
|
|
"country": "國家",
|
|
|
|
|
|
"server": "伺服器",
|
|
|
|
|
|
"city": "城市",
|
|
|
|
|
|
"allCities": "所有城市",
|
|
|
|
|
|
"privateKey": "私密金鑰",
|
|
|
|
|
|
"load": "負載"
|
|
|
|
|
|
},
|
|
|
|
|
|
"balancer": {
|
|
|
|
|
|
"addBalancer": "新增負載均衡",
|
|
|
|
|
|
"editBalancer": "編輯負載均衡",
|
|
|
|
|
|
"balancerStrategy": "策略",
|
|
|
|
|
|
"balancerSelectors": "選擇器",
|
|
|
|
|
|
"tag": "標籤",
|
|
|
|
|
|
"tagDesc": "唯一標籤",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"tagDuplicate": "該標籤已被其他均衡器使用",
|
|
|
|
|
|
"tagPlaceholder": "唯一均衡器標籤",
|
|
|
|
|
|
"selector": "選擇器",
|
|
|
|
|
|
"fallback": "Fallback",
|
|
|
|
|
|
"expected": "期望",
|
|
|
|
|
|
"expectedPlaceholder": "最佳節點數",
|
|
|
|
|
|
"maxRtt": "最大 RTT",
|
|
|
|
|
|
"tolerance": "容差",
|
|
|
|
|
|
"baselines": "Baselines",
|
|
|
|
|
|
"costs": "Costs",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"balancerDesc": "無法同時使用 balancerTag 和 outboundTag。如果同時使用,則只有 outboundTag 會生效。"
|
|
|
|
|
|
},
|
|
|
|
|
|
"wireguard": {
|
|
|
|
|
|
"secretKey": "金鑰",
|
|
|
|
|
|
"publicKey": "公鑰",
|
|
|
|
|
|
"allowedIPs": "允許的 IP",
|
|
|
|
|
|
"endpoint": "端點",
|
|
|
|
|
|
"psk": "共享金鑰",
|
|
|
|
|
|
"domainStrategy": "域策略"
|
|
|
|
|
|
},
|
|
|
|
|
|
"tun": {
|
|
|
|
|
|
"nameDesc": "TUN 介面的名稱。預設值為 'xray0'",
|
|
|
|
|
|
"mtuDesc": "最大傳輸單元。資料包的最大大小。預設值為 1500",
|
|
|
|
|
|
"userLevel": "用戶級別",
|
|
|
|
|
|
"userLevelDesc": "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
|
|
|
|
|
|
},
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"nord": {
|
|
|
|
|
|
"accessToken": "Access token",
|
|
|
|
|
|
"privateKey": "私鑰",
|
|
|
|
|
|
"noServers": "未找到選定國家/地區的伺服器",
|
|
|
|
|
|
"noPublicKey": "選定的伺服器未公布 NordLynx 公鑰。",
|
|
|
|
|
|
"outboundAdded": "NordVPN 出站已新增",
|
|
|
|
|
|
"outboundUpdated": "NordVPN 出站已更新"
|
|
|
|
|
|
},
|
|
|
|
|
|
"warp": {
|
|
|
|
|
|
"licenseError": "設定 WARP 授權失敗。",
|
|
|
|
|
|
"fetchFirst": "請先取得 WARP 設定。",
|
|
|
|
|
|
"createAccount": "建立 WARP 帳號",
|
|
|
|
|
|
"accessToken": "Access token",
|
|
|
|
|
|
"deviceId": "裝置 ID",
|
|
|
|
|
|
"licenseKey": "授權金鑰",
|
|
|
|
|
|
"privateKey": "私鑰",
|
|
|
|
|
|
"deleteAccount": "刪除帳號",
|
|
|
|
|
|
"settings": "設定",
|
|
|
|
|
|
"licenseKeyLabel": "WARP / WARP+ 授權金鑰",
|
|
|
|
|
|
"key": "金鑰",
|
|
|
|
|
|
"keyPlaceholder": "26 位 WARP+ 金鑰",
|
|
|
|
|
|
"accountInfo": "帳號資訊",
|
|
|
|
|
|
"deviceName": "裝置名稱",
|
|
|
|
|
|
"deviceModel": "裝置型號",
|
|
|
|
|
|
"deviceEnabled": "裝置已啟用",
|
|
|
|
|
|
"accountType": "帳號類型",
|
|
|
|
|
|
"role": "角色",
|
|
|
|
|
|
"warpPlusData": "WARP+ 資料",
|
|
|
|
|
|
"quota": "配額",
|
|
|
|
|
|
"usage": "使用",
|
|
|
|
|
|
"addOutbound": "新增出站"
|
|
|
|
|
|
},
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"dns": {
|
|
|
|
|
|
"enable": "啟用 DNS",
|
|
|
|
|
|
"enableDesc": "啟用內建 DNS 伺服器",
|
|
|
|
|
|
"tag": "DNS 入站標籤",
|
|
|
|
|
|
"tagDesc": "此標籤將在路由規則中可用作入站標籤",
|
|
|
|
|
|
"clientIp": "客戶端IP",
|
|
|
|
|
|
"clientIpDesc": "用於在DNS查詢期間通知伺服器指定的IP位置",
|
|
|
|
|
|
"disableCache": "禁用快取",
|
|
|
|
|
|
"disableCacheDesc": "禁用DNS快取",
|
|
|
|
|
|
"disableFallback": "禁用回退",
|
|
|
|
|
|
"disableFallbackDesc": "禁用回退DNS查詢",
|
|
|
|
|
|
"disableFallbackIfMatch": "匹配時禁用回退",
|
|
|
|
|
|
"disableFallbackIfMatchDesc": "當DNS伺服器的匹配域名列表命中時,禁用回退DNS查詢",
|
|
|
|
|
|
"enableParallelQuery": "啟用並行查詢",
|
|
|
|
|
|
"enableParallelQueryDesc": "啟用並行DNS查詢到多個伺服器以實現更快的解析",
|
|
|
|
|
|
"strategy": "查詢策略",
|
|
|
|
|
|
"strategyDesc": "解析域名的總體策略",
|
|
|
|
|
|
"add": "新增伺服器",
|
|
|
|
|
|
"edit": "編輯伺服器",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"domains": "網域",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"expectIPs": "預期 IP",
|
|
|
|
|
|
"unexpectIPs": "意外IP",
|
|
|
|
|
|
"useSystemHosts": "使用系統Hosts",
|
|
|
|
|
|
"useSystemHostsDesc": "使用已安裝系統的hosts檔案",
|
2026-05-10 15:03:11 +00:00
|
|
|
|
"serveStale": "提供過期結果",
|
|
|
|
|
|
"serveStaleDesc": "在背景重新整理時傳回過期的快取結果",
|
|
|
|
|
|
"serveExpiredTTL": "過期TTL",
|
|
|
|
|
|
"serveExpiredTTLDesc": "過期快取項目的有效期(秒);0 = 永不過期",
|
|
|
|
|
|
"timeoutMs": "逾時 (毫秒)",
|
|
|
|
|
|
"skipFallback": "跳過回退",
|
|
|
|
|
|
"finalQuery": "最終查詢",
|
|
|
|
|
|
"hosts": "Hosts",
|
|
|
|
|
|
"hostsAdd": "新增 Host",
|
|
|
|
|
|
"hostsEmpty": "未定義任何 Host",
|
|
|
|
|
|
"hostsDomain": "網域 (例如 domain:example.com)",
|
|
|
|
|
|
"hostsValues": "IP 或網域 — 輸入後按 Enter",
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a
standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS
inbound, the way Xray models them natively. Rip out the entire feature
cleanly (no migration needed since it was never released): protocol
constant, fallback children DB table, FallbackService, 2 API endpoints,
all UI rows, related translations and api-docs. A native fallback flow
attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links
A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a
fallback master: pick existing inbounds as children and the panel auto-
fills the SNI / ALPN / path / xver routing fields from each child's
transport, auto-builds settings.fallbacks at config-gen time, and
rewrites the child's client-share link so it advertises the master's
reachable endpoint and TLS state instead of the child's loopback listen.
Layout matches the Xray All-in-One Nginx example: master at :443 with
clients + TLS, each child on 127.0.0.1 with its own transport+clients.
Order matters (Xray walks fallbacks top-to-bottom) — reorder via the
per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row
Edit toggle for the rare cases where the auto-derivation needs
overriding; otherwise just pick a child and you're done.
Backend: new InboundFallback table + FallbackService (GetByMaster /
SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes
(GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig
injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality
inbound; GetInbounds annotates each child with FallbackParent so the
frontend can rewrite links without an extra round-trip.
Link projection covers every emission path — clients-page QR/links,
per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the
inbounds-page link/info/QR — via a shared projectThroughFallbackMaster
on the backend and a shared projectChildThroughMaster on the frontend
that both handle the panel-tracked relationship and the legacy
unix-socket (@vless-ws) convention.
Strings translated into all 12 non-English locales.
* docs: rewrite CONTRIBUTING with full local-dev setup
The prior three-line CONTRIBUTING left newcomers guessing at every
non-trivial step: which Go / Node versions, where xray comes from, why
the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue
multi-page setup is wired, what to do on Windows when go build trips
on the CGo SQLite driver.
Now covers prerequisites, MinGW-w64 install on Windows (niXman builds
or MSYS2), one-shot first-time setup, two frontend dev workflows with
the XUI_DEBUG asset-cache gotcha called out, the architecture and
conventions of the Vue side, a project-layout map, useful env vars,
and the PR checklist.
---------
2026-05-19 10:16:42 +00:00
|
|
|
|
"usePreset": "使用範本",
|
|
|
|
|
|
"dnsPresetTitle": "DNS範本",
|
|
|
|
|
|
"dnsPresetFamily": "家庭",
|
2026-05-10 15:03:11 +00:00
|
|
|
|
"clearAll": "全部刪除",
|
|
|
|
|
|
"clearAllTitle": "刪除所有 DNS 伺服器?",
|
|
|
|
|
|
"clearAllConfirm": "此操作將從清單中刪除所有 DNS 伺服器,無法復原。"
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
},
|
|
|
|
|
|
"fakedns": {
|
|
|
|
|
|
"add": "新增假 DNS",
|
|
|
|
|
|
"edit": "編輯假 DNS",
|
|
|
|
|
|
"ipPool": "IP 池子網",
|
|
|
|
|
|
"poolSize": "池大小"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
"tgbot": {
|
|
|
|
|
|
"keyboardClosed": "❌ 自定義鍵盤已關閉!",
|
|
|
|
|
|
"noResult": "❗ 沒有結果!",
|
|
|
|
|
|
"noQuery": "❌ 未找到查詢!請再次使用該命令!",
|
|
|
|
|
|
"wentWrong": "❌ 出了點問題!",
|
|
|
|
|
|
"noIpRecord": "❗ 沒有IP記錄!",
|
|
|
|
|
|
"noInbounds": "❗ 未找到入站!",
|
|
|
|
|
|
"unlimited": "♾ 無限(重置)",
|
|
|
|
|
|
"add": "添加",
|
|
|
|
|
|
"month": "月",
|
|
|
|
|
|
"months": "月",
|
|
|
|
|
|
"day": "天",
|
|
|
|
|
|
"days": "天",
|
|
|
|
|
|
"hours": "小時",
|
|
|
|
|
|
"minutes": "分鐘",
|
|
|
|
|
|
"unknown": "未知",
|
|
|
|
|
|
"inbounds": "入站",
|
|
|
|
|
|
"clients": "客戶端",
|
|
|
|
|
|
"offline": "🔴 離線",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"online": "🟢 上線",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"commands": {
|
|
|
|
|
|
"unknown": "❗ 未知命令",
|
|
|
|
|
|
"pleaseChoose": "👇 請選擇:\r\n",
|
|
|
|
|
|
"help": "🤖 歡迎使用本機器人!它旨在為您提供來自伺服器的特定資料,並允許您進行必要的修改。\r\n\r\n",
|
|
|
|
|
|
"start": "👋 你好,<i>{{ .Firstname }}</i>。\r\n",
|
|
|
|
|
|
"welcome": "🤖 歡迎來到 <b>{{ .Hostname }}</b> 管理機器人。\r\n",
|
|
|
|
|
|
"status": "✅ 機器人正常執行!",
|
|
|
|
|
|
"usage": "❗ 請輸入要搜尋的文字!",
|
|
|
|
|
|
"getID": "🆔 您的 ID 為:<code>{{ .ID }}</code>",
|
|
|
|
|
|
"helpAdminCommands": "要重新啟動 Xray Core:\r\n<code>/restart</code>\r\n\r\n要搜尋客戶電子郵件:\r\n<code>/usage [電子郵件]</code>\r\n\r\n要搜尋入站(帶有客戶統計資料):\r\n<code>/inbound [備註]</code>\r\n\r\nTelegram聊天ID:\r\n<code>/id</code>",
|
|
|
|
|
|
"helpClientCommands": "要搜尋統計資料,請使用以下命令:\r\n<code>/usage [電子郵件]</code>\r\n\r\nTelegram聊天ID:\r\n<code>/id</code>",
|
|
|
|
|
|
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
|
|
|
|
|
"restartSuccess": "✅ 操作成功!",
|
|
|
|
|
|
"restartFailed": "❗ 操作錯誤。\r\n\r\n<code>錯誤: {{ .Error }}</code>.",
|
|
|
|
|
|
"xrayNotRunning": "❗ Xray Core 未運行。",
|
|
|
|
|
|
"startDesc": "顯示主選單",
|
|
|
|
|
|
"helpDesc": "機器人幫助",
|
|
|
|
|
|
"statusDesc": "檢查機器人狀態",
|
|
|
|
|
|
"idDesc": "顯示您的 Telegram ID"
|
|
|
|
|
|
},
|
|
|
|
|
|
"messages": {
|
|
|
|
|
|
"cpuThreshold": "🔴 CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%",
|
|
|
|
|
|
"selectUserFailed": "❌ 使用者選擇錯誤!",
|
|
|
|
|
|
"userSaved": "✅ 電報使用者已儲存。",
|
|
|
|
|
|
"loginSuccess": "✅ 成功登入到面板。\r\n",
|
|
|
|
|
|
"loginFailed": "❗️ 面板登入失敗。\r\n",
|
|
|
|
|
|
"2faFailed": "2FA 失敗",
|
|
|
|
|
|
"report": "🕰 定時報告:{{ .RunTime }}\r\n",
|
|
|
|
|
|
"datetime": "⏰ 日期時間:{{ .DateTime }}\r\n",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"hostname": "💻 主機: {{ .Hostname }}\r\n",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"version": "🚀 X-UI 版本:{{ .Version }}\r\n",
|
|
|
|
|
|
"xrayVersion": "📡 Xray 版本: {{ .XrayVersion }}\r\n",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
|
|
|
|
|
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
|
|
|
|
|
"ip": "🌐 IP: {{ .IP }}\r\n",
|
|
|
|
|
|
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"serverUpTime": "⏳ 伺服器執行時間:{{ .UpTime }} {{ .Unit }}\r\n",
|
|
|
|
|
|
"serverLoad": "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
|
|
|
|
|
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
|
|
|
|
|
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"traffic": "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"xrayStatus": "ℹ️ 狀態: {{ .State }}\r\n",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"username": "👤 使用者名稱:{{ .Username }}\r\n",
|
|
|
|
|
|
"reason": "❗️ 原因:{{ .Reason }}\r\n",
|
|
|
|
|
|
"time": "⏰ 時間:{{ .Time }}\r\n",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"inbound": "📍 入站: {{ .Remark }}\r\n",
|
|
|
|
|
|
"port": "🔌 連接埠: {{ .Port }}\r\n",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"expire": "📅 過期日期:{{ .Time }}\r\n",
|
|
|
|
|
|
"expireIn": "📅 剩餘時間:{{ .Time }}\r\n",
|
|
|
|
|
|
"active": "💡 啟用:{{ .Enable }}\r\n",
|
|
|
|
|
|
"enabled": "🚨 已啟用:{{ .Enable }}\r\n",
|
|
|
|
|
|
"online": "🌐 連線狀態:{{ .Status }}\r\n",
|
|
|
|
|
|
"lastOnline": "🔙 上次上線: {{ .Time }}\r\n",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"email": "📧 電子郵件: {{ .Email }}\r\n",
|
|
|
|
|
|
"upload": "🔼 上傳: ↑{{ .Upload }}\r\n",
|
|
|
|
|
|
"download": "🔽 下載: ↓{{ .Download }}\r\n",
|
|
|
|
|
|
"total": "📊 總計: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"TGUser": "👤 電報使用者:{{ .TelegramID }}\r\n",
|
|
|
|
|
|
"exhaustedMsg": "🚨 耗盡的 {{ .Type }}:\r\n",
|
|
|
|
|
|
"exhaustedCount": "🚨 耗盡的 {{ .Type }} 數量:\r\n",
|
|
|
|
|
|
"onlinesCount": "🌐 線上客戶:{{ .Count }}\r\n",
|
|
|
|
|
|
"disabled": "🛑 禁用:{{ .Disabled }}\r\n",
|
|
|
|
|
|
"depleteSoon": "🔜 即將耗盡:{{ .Deplete }}\r\n\r\n",
|
|
|
|
|
|
"backupTime": "🗄 備份時間:{{ .Time }}\r\n",
|
|
|
|
|
|
"refreshedOn": "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n",
|
|
|
|
|
|
"yes": "✅ 是的",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"no": "❌ 否",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"received_id": "🔑📥 ID 已更新。",
|
|
|
|
|
|
"received_password": "🔑📥 密碼已更新。",
|
|
|
|
|
|
"received_email": "📧📥 電子郵件已更新。",
|
|
|
|
|
|
"received_comment": "💬📥 評論已更新。",
|
|
|
|
|
|
"id_prompt": "🔑 預設 ID: {{ .ClientId }}\n\n請輸入您的 ID。",
|
|
|
|
|
|
"pass_prompt": "🔑 預設密碼: {{ .ClientPassword }}\n\n請輸入您的密碼。",
|
|
|
|
|
|
"email_prompt": "📧 預設電子郵件: {{ .ClientEmail }}\n\n請輸入您的電子郵件。",
|
|
|
|
|
|
"comment_prompt": "💬 預設評論: {{ .ClientComment }}\n\n請輸入您的評論。",
|
|
|
|
|
|
"inbound_client_data_id": "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了!",
|
|
|
|
|
|
"inbound_client_data_pass": "🔄 入站: {{ .InboundRemark }}\n\n🔑 密碼: {{ .ClientPass }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了!",
|
|
|
|
|
|
"cancel": "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄",
|
|
|
|
|
|
"error_add_client": "⚠️ 錯誤:\n\n {{ .error }}",
|
|
|
|
|
|
"using_default_value": "好的,我會使用預設值。 😊",
|
|
|
|
|
|
"incorrect_input": "您的輸入無效。\n短語應連續輸入,不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫",
|
|
|
|
|
|
"AreYouSure": "你確定嗎?🤔",
|
|
|
|
|
|
"SuccessResetTraffic": "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功",
|
|
|
|
|
|
"FailedResetTraffic": "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ 錯誤: [ {{ .ErrorMessage }} ]",
|
|
|
|
|
|
"FinishProcess": "🔚 所有客戶的流量重置已完成。"
|
|
|
|
|
|
},
|
|
|
|
|
|
"buttons": {
|
|
|
|
|
|
"closeKeyboard": "❌ 關閉鍵盤",
|
|
|
|
|
|
"cancel": "❌ 取消",
|
|
|
|
|
|
"cancelReset": "❌ 取消重置",
|
|
|
|
|
|
"cancelIpLimit": "❌ 取消 IP 限制",
|
|
|
|
|
|
"confirmResetTraffic": "✅ 確認重置流量?",
|
|
|
|
|
|
"confirmClearIps": "✅ 確認清除 IP?",
|
|
|
|
|
|
"confirmRemoveTGUser": "✅ 確認移除 Telegram 使用者?",
|
|
|
|
|
|
"confirmToggle": "✅ 確認啟用/禁用使用者?",
|
|
|
|
|
|
"dbBackup": "獲取資料庫備份",
|
|
|
|
|
|
"serverUsage": "伺服器使用情況",
|
|
|
|
|
|
"getInbounds": "獲取入站資訊",
|
|
|
|
|
|
"depleteSoon": "即將耗盡",
|
|
|
|
|
|
"clientUsage": "獲取使用情況",
|
|
|
|
|
|
"onlines": "線上客戶端",
|
|
|
|
|
|
"commands": "命令",
|
|
|
|
|
|
"refresh": "🔄 重新整理",
|
|
|
|
|
|
"clearIPs": "❌ 清除 IP",
|
|
|
|
|
|
"removeTGUser": "❌ 移除 Telegram 使用者",
|
|
|
|
|
|
"selectTGUser": "👤 選擇 Telegram 使用者",
|
|
|
|
|
|
"selectOneTGUser": "👤 選擇一個 Telegram 使用者:",
|
|
|
|
|
|
"resetTraffic": "📈 重置流量",
|
|
|
|
|
|
"resetExpire": "📅 更改到期日期",
|
|
|
|
|
|
"ipLog": "🔢 IP 日誌",
|
|
|
|
|
|
"ipLimit": "🔢 IP 限制",
|
|
|
|
|
|
"setTGUser": "👤 設定 Telegram 使用者",
|
|
|
|
|
|
"toggle": "🔘 啟用/禁用",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"custom": "🔢 自訂",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"confirmNumber": "✅ 確認: {{ .Num }}",
|
|
|
|
|
|
"confirmNumberAdd": "✅ 確認新增:{{ .Num }}",
|
|
|
|
|
|
"limitTraffic": "🚧 流量限制",
|
|
|
|
|
|
"getBanLogs": "禁止日誌",
|
|
|
|
|
|
"allClients": "所有客戶",
|
|
|
|
|
|
"addClient": "新增客戶",
|
|
|
|
|
|
"submitDisable": "以停用方式送出 ☑️",
|
|
|
|
|
|
"submitEnable": "以啟用方式送出 ✅",
|
|
|
|
|
|
"use_default": "🏷️ 使用預設值",
|
|
|
|
|
|
"change_id": "⚙️🔑 ID",
|
|
|
|
|
|
"change_password": "⚙️🔑 密碼",
|
|
|
|
|
|
"change_email": "⚙️📧 電子郵件",
|
|
|
|
|
|
"change_comment": "⚙️💬 評論",
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
"change_flow": "⚙️🚦 Flow",
|
Vue3 migration (#4198)
* docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area
Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite
migration: 17,650 lines across 69 templates, 3,145 a-* component
instances across 63 files, with per-pattern counts and file lists.
Key findings:
- No Vue filters anywhere — dodges a major Vue 3 breaking change
- 358 v-model uses; AD-Vue 4 absorbs most, custom components don't
- 233 <template slot="X"> usages must become <template #X>
- 49 scopedSlots: { ... } column defs need new slots: { ... } shape
- a-icon is removed in AD-Vue 4 — every icon must be imported
Establishes the 8-phase order; Phase 2 (Vite toolchain) is next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4
Adds a frontend/ directory that lives alongside the legacy web/html/
Vue 2 templates during the migration. Vite builds into ../web/dist/
so the Go binary will be able to embed the result via embed.FS once
Phase 4 starts moving real pages over.
- package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10
- vite.config.js: dev server on :5173 with API proxy to the Go panel
on :2053; build output to ../web/dist/
- src/App.vue is currently a smoke-test placeholder — delete once the
first real page (login) lands in Phase 4
- node_modules and dist are already ignored at repo root
To verify locally:
cd frontend && npm install && npm run dev
Pages will be migrated one at a time on the vue3-migration branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules
Ports the framework-agnostic JS from web/assets/js/ into frontend/src/
so Vue 3 pages can import what they need without relying on script-tag
globals.
- web/assets/js/util/index.js (927 lines, 21 classes) →
frontend/src/utils/legacy.js + a barrel at utils/index.js. All
classes are now named exports.
- Vue.prototype.$message in HttpUtil → direct import of `message`
from ant-design-vue (Vue 3 has no Vue.prototype).
- RandomUtil.randomShadowsocksPassword previously defaulted to
SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular
import. Replaced with the literal string default.
- MediaQueryMixin (Vue 2 mixin) removed. Replaced by
composables/useMediaQuery.js — Vue 3 composable returning reactive
`isMobile`.
- axios-init.js wrapped as setupAxios(); Qs global → npm `qs`.
- websocket.js exported as WebSocketClient class; the implicit
window.wsClient global is gone — pages instantiate it themselves.
- model/{inbound,outbound,dbinbound,setting,reality_targets}.js
copied with `export` added on every top-level declaration. Imports
between models and utils are wired up explicitly.
- subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util).
- App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard /
useMediaQuery so the user can verify Phase 3 with `npm run dev`.
Run `cd frontend && npm install && npm run dev` — qs was added so a
fresh install is required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8
First real page in the new toolchain. Multi-page Vite: each migrated
page is its own entry. login.html now lives at frontend/login.html with
a thin entrypoint at frontend/src/login.js mounting LoginPage.vue.
Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+.
@vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue
stays on 4.2.6 — there is no AD-Vue 6.
Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page:
- new Vue({ el, delimiters, data, methods }) → createApp + <script setup>
- mounted() → onMounted()
- <template slot="X"> → <template #X>
- <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined />
</template> with explicit @ant-design/icons-vue imports
- v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs)
Three legacy features deferred so Phase 4 stays small:
- i18n (Phase 7 wires up vue-i18n)
- theme switcher (custom component pending Phase 5)
- headline word-cycle animation (purely aesthetic)
Run `cd frontend && npm install && npm run dev`, open
http://localhost:5173/login.html. With Go panel running on :2053 the
form submits real credentials via the configured proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11
Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale
lockfile; clean install resolves the new constraint). Bumps vue-i18n
to 11.1.4 since v10 was just EOL'd.
Migrates aThemeSwitch.html — the two-flavor theme picker + global
themeSwitcher object — into:
- composables/useTheme.js: single reactive `theme` state with
toggleTheme / toggleUltra. Boot side-effect applies the stored theme
to <body>/<html> before Vue renders; watchEffect persists changes
back to localStorage.
- components/ThemeSwitch.vue: full menu version for the main panel.
- components/ThemeSwitchLogin.vue: login-popover version.
AD-Vue 1 → 4 changes hit on this component:
- <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by
explicit BulbFilled / BulbOutlined imports from
@ant-design/icons-vue, swapped via <component :is="BulbIcon">
- Vue.component('a-theme-switch', { ... }) global registration → SFC
+ per-page import
- this.$message.config(...) (Vue 2 instance method) → message.config(...)
imported from ant-design-vue, called once in login.js at boot
Login page now surfaces a settings button → popover → theme picker.
Known gap: web/assets/css/custom.min.css isn't yet imported into the
new bundle, so toggling dark mode currently only re-themes AD-Vue's
own components, not the panel chrome. The body class is still toggled
so behavior is correct; visual fidelity returns when custom.css is
ported or directly imported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded
The /login proxy entry was matching any path starting with /login —
including /login.html, which Vite is supposed to serve itself. Without
the Go backend running, this caused ECONNREFUSED noise on every page
load.
Switched to regex patterns anchored with ^...$ so only the bare backend
paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes
(/panel/*, /server/*) get proxied. Static .html files Vite serves
directly are no longer matched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise
Two issues from running login.html against no Go backend:
1. Dark mode toggled the body class but didn't actually re-theme any
AD-Vue components. The legacy panel relied on custom.min.css which
we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap
LoginPage in <a-config-provider :theme="{ algorithm }"> driven by
our useTheme state, and AD-Vue restyles every component for free.
Page chrome (background, card, title) gets explicit .is-dark CSS
since the algorithm only covers AD-Vue components.
2. Vite logged every failed proxy attempt loudly. When the Go panel
isn't running locally that's pure noise. Added a configure()
callback that swallows ECONNREFUSED specifically; real errors
(timeouts, 5xx, anything else) still surface.
Both fixes are dev-experience only — production build is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): use legacy panel palette for login page dark mode
Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card)
that didn't match the rest of the panel. Replaced with the actual
values from web/assets/css/custom.min.css:
light dark ultra-dark
bg #c7ebe2 bg #222d42 bg #0f2d32
card #fff card #151f31 card #0c0e12
title #008771 title #fff/.92 title #fff/.92
Drove everything off CSS custom properties on .login-app so the
.is-dark / .is-ultra class swap is a few var overrides instead of
duplicating selectors. Also restored the legacy card metrics
(2rem radius, 4rem 3rem padding, 2rem title) so the new page
matches the old panel's geometry, not just its colors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave layout + recolor for dark mode
The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so
in dark/ultra-dark mode it rendered as a pale-white blob against the
dark page. Stripped the inline fills, drove them off CSS variables
that swap with .is-dark / .is-ultra:
light: green tints + #c7ebe2 (mint) on the bottom wave
dark: #222d42 across all four waves
ultra-dark: #0f2d32
The wave was also positioned wrong — anchored to the top 200px of
the viewport with absolute positioning. Restored the legacy layout:
- .waves-header is fixed to the top of the viewport with z-index -1
so the form floats over it
- .waves-inner-header pushes the wave SVG down to ~50vh with a
50vh-tall solid block of the page color
- .waves SVG itself is 15vh tall, sitting at the bottom of that block
Net effect: top half is solid-colored, then a wavy edge transitions
into the rest of the page, with the form centered on top — matching
the legacy panel exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): bring wave-header to front so the wave actually shows
Two layering bugs were hiding the wave entirely:
1. .ant-layout-content had background: var(--bg-page) which painted an
opaque rectangle covering the full content area — including the
fixed wave-header behind it. Made the layout/content transparent
and moved the bg paint up to .login-app (the outer ant-layout).
2. .waves-header had z-index: -1 which on its own was fine, but with
.ant-layout-content opaque on top it was doubly buried. Promoted
the wave-header to z-index: 0 and gave the form .login-row
z-index: 1, so the form sits above the wave and the wave sits
above the page-bg.
Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the
bottom half of the page below the wave matches the legacy panel
(was white). Dark mode stays at the surface-100/login-wave palette.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): match legacy wave animation timings + dark page bg
Two reasons the bottom wave looked static in dark/ultra-dark:
1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s.
The 20s on the bottom wave was so slow that against the low dark-
mode contrast it read as motionless. Restored the legacy timings.
2. --bg-page in dark mode was #151f31 (card color / surface-100), but
the legacy .under uses surface-200 (#222d42) — that's the color of
the bottom half of the page, the same as the wave fill, so the
wave appears to flow into the page rather than meeting a hard edge.
Now it does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): restore Hello/Welcome headline cycle on login
Earlier I deferred the legacy headline word-cycling animation as
"purely aesthetic". Restored it: the title now alternates between
'Hello' and 'Welcome' every 2 seconds, matching the legacy panel.
The legacy implementation toggled .is-visible / .is-hidden classes on
two <b> elements via setTimeout chains and DOM querying. Replaced
with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade
between words is declarative — no manual DOM manipulation, and the
interval is properly cleaned up in onBeforeUnmount.
The earlier "Welcome to 3x-ui" string was wrong on two counts: it
should be just "Welcome", and it should be one of two cycling words
with "Hello" preceding it.
Ultra-dark palette already matched legacy after the prior wave timing
fix; no additional changes needed there beyond the animation speeds
that now also apply to ultra-dark via the shared CSS rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): correct dark login bg + give ultra-dark wave real contrast
Two related fixes:
1. Default-dark wave-header bg was wrong. I had #0a2227, but that's
the *ultra-dark* override; default dark uses --dark-color-background
= #0a1222. Now the dark-mode top half is the legacy purple-blue
instead of teal.
2. Ultra-dark wave fill is intentionally near-identical to its bg in
the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which
makes the wave look static even though the animation is running.
Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark
only — far enough above the bg that the motion reads, while
staying within the same teal hue family.
Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly
#0c0e12, which is the card color, not the page color).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): drop ultra-dark bottom-wave seam line
Last fix made the wave fill #1f4d52 in ultra-dark for both top-three
waves and the bottom wave, which gave visible motion but exposed a
hard horizontal line where the bottom wave's flat lower edge met the
page bg (#0f2d32). The user noticed it as "the wave at the bottom
not moving its like a line" — they were seeing the SVG's clipped
bottom edge, not the wave itself.
Solution: only the top three waves get the brighter fill (those carry
the visible motion). The bottom wave reverts to #0f2d32 = --bg-page,
so its flat bottom edge merges seamlessly into the page below. Net
effect: motion is still visible (from waves 2 and 3), and there's no
seam line at the bottom of the SVG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5c-i — index.html dashboard shell
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>
* feat(frontend): Phase 5c-ii — live status cards on the dashboard
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>
* feat(frontend): Phase 5c-iii — xray status card + stop/restart controls
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>
* feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals
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>
* feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals
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>
* feat(frontend): Phase 5c-v — custom-geo section in VersionModal
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>
* feat(frontend): Phase 5d-i — settings page shell + dirty tracking
Adds the settings entry as a new Vite multi-page input. Lays down the
shared page chrome (sidebar, save bar, restart, security alert) and the
AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop
in tab partials without re-implementing it.
- settings.html + src/settings.js: third Vite entry; mounts SettingsPage.
- SettingsPage.vue: page chrome with the legacy two-button save/restart
bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats
tab gated on subJsonEnable || subClashEnable). Each tab body is an
a-empty placeholder until 5d-ii…vi fill them in.
- useAllSetting.js composable: POST /panel/setting/all on mount, mirrors
the legacy 1s busy-loop dirty check via setInterval, and exposes
fetchAll/saveAll. saveDisabled flips off as soon as the user diverges
from the server snapshot.
- restartPanel rebuilds the URL (host/port/scheme/base path) from the
saved settings so users land on the new endpoint after a port or
cert change.
- models/setting.js: adopts the @/utils alias and a leading file-level
doc — semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-ii — settings General tab
Ports the panel/general partial (the largest single tab) — six
collapse panels: General, Notifications, Certificates, External
traffic webhook, Date and time, LDAP.
- GeneralTab.vue receives the reactive AllSetting via props and binds
fields directly with v-model:value; SettingsPage stays the sole
fetch/save owner.
- remarkModel/remarkSeparator surfaced as computed v-models that
read+write the underlying single-string field (legacy stores them
packed as <separator><orderedKeys>, e.g. "-ieo").
- LDAP inbound-tags select binds to a CSV ↔ array computed; inbound
options come from /panel/api/inbounds/list on mount.
- Language select stays cookie-based via LanguageManager and reloads
on change — same UX as legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal
Ports the panel/security partial: change-credentials form and 2FA
toggle. The 2FA modal is a new shared component since enabling 2FA,
disabling 2FA, and changing credentials all funnel through it with
slightly different copy.
- TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a
6-digit verifier; 'confirm' flow renders just the verifier. The
parent passes a confirm(success) callback that fires only when the
entered code matches the live TOTP value (otpauth lib).
- SecurityTab.vue: holds the local user form (oldUsername/oldPassword/
new*), POSTs /panel/setting/updateUser, and on success force-redirects
to logout. When 2FA is on, the credentials change goes through the
confirm-modal first.
- toggleTwoFactor leaves the switch read-only (the v-bound :checked
matches AllSetting) and only flips after the modal succeeds, so
cancelling out leaves state unchanged.
- Adds otpauth ^9.5.1 dep (qrious was already present).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-iv — settings Telegram tab
Ports the panel/telegram partial: bot enable/token/chatId/lang in the
General panel, schedule/backup/login/CPU-threshold in Notifications,
and proxy/API-server overrides in the third panel. All bindings live
on the shared AllSetting reactive — no fetch/save logic in this tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-v — settings Subscription general tab
Ports the subscription/general partial — four collapse panels covering
the master enable switches, presentation/template fields, certs, and
update interval.
- Sub path goes through a strip-on-input + normalize-on-blur computed:
legacy stripped `:` and `*` and ensured the value starts and ends
with a single `/` — same here.
- Both `subEnableRouting` and the announce/profile/title/support URLs
are bound directly on AllSetting.
- The "Subscription URI override" placeholder mirrors the legacy
pattern for the manual full-URL form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5d-vi — settings Subscription formats tab
Ports the subscription/json partial — paths/URIs for the JSON and
Clash formats plus the four packed-JSON sub-fields: fragment, noises,
mux, and direct routing rules.
- subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each
a JSON string on the wire; the tab exposes their fields as computed
v-models that read+write the underlying JSON. Toggling a top-level
switch off resets the field to "" (matches legacy semantics).
- Direct routing rules surface the IP and domain entries of the seed
rule array as multi-select tag inputs; setting/removing tags
edits the rules array in place rather than rebuilding it from
scratch, so manually-added rules are preserved.
- Tab is gated on subJsonEnable || subClashEnable in the parent (only
rendered when the user actually opted into one of those formats).
This closes Phase 5d — full settings page parity with the legacy panel
across all five tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): route /panel/<route> to migrated pages in dev
The sidebar links to production-style URLs like /panel/settings, but
in dev that gets proxied to the legacy Go template — which fails
because we haven't loaded the legacy asset chain. Add a proxy bypass
so /panel and /panel/settings are served from index.html / settings.html
on the Vite dev server itself. Unmigrated routes (inbounds, xray)
still proxy to Go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(csrf): expose token endpoint for SPA pages and fetch it from axios
The legacy panel pages got their CSRF token from a <meta name="csrf-token">
tag rendered by Go. SPA pages built by Vite don't have that, so every
unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware
with no token and getting 403 — visible as the settings page being
stuck on "Loading…" because POST /panel/setting/all failed.
- web/controller/xui.go: GET /panel/csrf-token returns the session
token. Lives under the xui group so checkLogin still gates it; the
CSRFMiddleware on the same group is a no-op for GET.
- frontend/src/api/axios-init.js: cache the token at module scope and
lazy-fetch it when a non-safe request needs one. Seed from the meta
tag first when present (legacy compat). On a 403 response, drop the
cache and retry once — handles the case where a server restart
rotated the token after the SPA loaded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): keep sidebar links absolute when basePath is empty
The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev
the window-injected basePath is '' so the resulting key was a relative
path like 'panel/settings'. When the browser resolved that against the
current /panel/settings URL it produced /panel/panel/settings — visible
as broken navigation between Dashboard and Settings.
Force a leading slash so the keys are always absolute regardless of
whether the host injected a basePath.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-i — inbounds page shell + list fetch
Adds the inbounds entry as a fourth Vite multi-page input and wires
/panel/inbounds through the dev proxy bypass. Lays down the page
chrome (sidebar, summary statistics card, refresh button) and the
fetch lifecycle composable so 5f-ii onward can drop in the table
columns and the modals without re-implementing it.
- inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage.
- InboundsPage.vue: sidebar + summary card (totals over up/down,
all-time, inbound count, client tags) + a basic table with enable/
remark/port/protocol/traffic/expiry columns. Row actions, popovers,
search/filter, auto-refresh, and the WebSocket delta path are all
deferred to subsequent 5f subphases.
- useInbounds.js composable: GET /panel/api/inbounds/list +
POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline +
POST /panel/setting/defaultSettings, then computes the
per-inbound clientCount roll-ups (active/deactive/depleted/expiring/
online/comments) the table popovers consume.
- models/dbinbound.js + models/inbound.js: switched the legacy-utils
import to the @/utils alias for consistency with the rest of the
app. Semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh
Fleshes out the inbound list with the full column set, search & filter
toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id,
and a per-row action dropdown that emits events the parent will route
to modals as those land in 5f-iii through 5f-vii.
- InboundList.vue (new): toolbar (Add inbound + General actions
dropdown + Refresh + auto-refresh popover), search-or-filter switch
with the legacy radio buttons (Active/Disabled/Depleted/Depleting/
Online), and a a-table with desktop and mobile column variants.
Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/
allTime/expiry/info cells render the same popovers and tags as
legacy. Row enable switch is optimistic with rollback on POST
failure.
- visibleInbounds computed mirrors the legacy search and filter
projection: deep search through dbInbound + clients, or filter
reduces inbound.settings.clients to the selected bucket so the
table only shows matching client rows.
- Auto-refresh interval is read/written to localStorage with the
same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy
panel. WebSocket delta updates are still deferred.
- Action menu emits event payloads {key, dbInbound}; the parent
currently shows a "coming in later 5f subphase" toast for each.
Modals (edit/qr/clone/delete/reset/info/clients) land in
5f-iii through 5f-vii.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): wrap popover-table rows in <tbody>
Vue's template compiler warned that <tr> can't be a direct child of
<table> per the HTML spec; the browser silently inserts a <tbody>
wrapper but Vue's SSR/hydration path doesn't, which can cause
hydration mismatches. Add explicit <tbody> in both popover tables
(traffic + mobile-info).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset
Wires up the inbound CRUD flows. The protocol-specific and transport-
specific forms are still ahead in 5f-iii-b — for now the modal exposes
those as JSON textareas so users can both edit existing inbounds without
losing settings and create new ones from default templates.
- InboundFormModal.vue: tabbed modal with a full Basics tab (enable,
remark, protocol, listen, port, total GB, traffic reset, expiry
date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add
mode stamps a fresh template per protocol via
Inbound.Settings.getSettings(protocol); changing the protocol in
add mode restamps the JSON. Edit mode pretty-prints the existing
JSON so the user sees the same fields they save back.
- POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on
submit; on success the parent refreshes the list and the modal
closes. Malformed JSON in any of the three textareas surfaces a
message.error and aborts the save without losing user input.
- InboundsPage.vue: wires the row action menu to real handlers —
edit (opens the modal in edit mode), delete, reset-traffic,
clone, reset-clients, del-depleted-clients all go through
Modal.confirm and refresh on success. General actions menu wires
reset-inbounds / reset-clients / del-depleted-clients the same way.
Remaining actions (qrcode/info/import/export/copyClients) still
toast as "coming soon" — those land in 5f-iv and 5f-v.
- Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals
Wires per-inbound client management. Both flows go through the same
addClient/updateClient endpoints as legacy; the modals just funnel
the form state into the right shape (`{id, settings: '{"clients": [...]}'}`).
- ClientFormModal.vue: protocol-aware single-client editor — email/
password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/
expiry/renewal fields are shown/hidden per protocol like legacy.
Edit mode displays the per-client traffic stats with a reset
button; IP-limit log is read on click and clearable. Random
helpers (sync icon next to each label) regenerate UUID/email/
password/sub-id values.
- ClientBulkModal.vue: 1–500 clients in one POST, with the legacy
five email-generation modes (Random / +Prefix / +Num / +Postfix /
Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware
factory and concatenates their toString() output into a single
settings.clients JSON array.
- InboundsPage.vue: opens both modals from the row action menu
(`addClient` / `addBulkClient`). They both refresh the inbound list
on success.
- Outstanding row actions still toast as "coming soon": qrcode,
showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-v — inbound info + QR-code modals
Wires the row "info" and "qrcode" actions and ports the legacy
inbound_info_modal end-to-end. The info modal handles every protocol
the legacy panel did:
• multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client
table + share links + per-link QR;
• SS single-user — share link + QR;
• WireGuard — full peer table with downloadable peer-N.conf and a
wg:// share link per peer;
• Mixed/HTTP/Tunnel — connection-detail tables.
- QrPanel.vue: shared link card (header tag, copy button, optional
download button, optional QR canvas, monospace footer with the
raw value). Per-instance QRious instances are repainted on
value/size change.
- InboundInfoModal.vue: full info modal. Subscription URL block keys
off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and
surfaces refresh + clear; tg-id, last-online, depleted/enabled tags
all match legacy.
- QrCodeModal.vue: lighter modal used for the row "qrcode" action on
SS-single and WireGuard inbounds (just the QRs, no info table).
- InboundsPage.vue: wires both flows. checkFallback() reproduces the
legacy logic — when an inbound listens on a unix-socket fallback
(`@<name>`), the link generator is pointed at the root inbound that
owns the listen address so QRs/links carry the public host:port +
the right TLS state. Multi-client navigation (focusing a specific
client's links) is deferred to 5f-vi where the per-inbound expand-
row table will pass the email through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vi — per-inbound client expand-row table
Each multi-user inbound row in the list now expands to show its
client roster, mirroring the legacy aClientTable component.
- ClientRowTable.vue: inner a-table with full desktop column set
(action icons / enable / online / client-with-status-dot / traffic
with progress bar / all-time / expiry with reset cycle) and a
collapsed mobile variant (single dropdown menu + popover info).
Self-contained: stats are looked up via a per-inbound email->stats
Map; per-client confirms (reset/delete) live on the row.
- The component emits typed events (edit/qrcode/info/reset-traffic/
delete/toggle-enable) — InboundsPage routes them back to the
existing client and info modals (with `findClientIndex` so the
modal opens focused on the right client).
- InboundList.vue: hooks ClientRowTable into the a-table's
expandedRowRender slot; row-class-name `hide-expand-icon` and a
scoped CSS rule hide the chevron for non-multi-user inbounds
(HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat.
- toggle-enable-client routes through updateClient with the same
`{id, settings: '{"clients": [...]}'}` shape as the other modals,
so backend parsing stays single-pathed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms
Rewrites InboundFormModal to look like the legacy panel: structured
forms for the common case, with a compact "Advanced (JSON)" fallback
for the rare bits we don't yet have UI for.
Tabs:
• Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry
• Protocol — protocol-aware:
VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline
first-client form (email + ID/password/auth, security, flow,
subId, comment, total GB, expiry);
edit mode shows a clients-count summary table;
VLess: decryption/encryption inputs;
SS: method dropdown that re-randomizes password and propagates
method change to the multi-user array (matches legacy
SSMethodChange);
HTTP/Mixed: accounts table with add/remove rows + Mixed
auth/udp/ip toggles;
Tunnel: address/port/network/followRedirect;
WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair)
+ per-peer fields with PSK regen + allowedIPs add/remove +
keepAlive.
• Stream — only when canEnableStream(); transport selector with
structured forms for TCP (proxy-protocol, http camouflage),
WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode),
HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab
with an alert banner. Security selector with TLS (sni/alpn/
fingerprint) and Reality (target/serverNames/keypair-gen via
/panel/api/server/getNewX25519Cert / shortIds / fingerprint).
• Sniffing — enabled/destOverride/metadataOnly/routeOnly/
ipsExcluded/domainsExcluded as structured fields.
• Advanced (JSON) — raw streamSettings + sniffing JSON for users
reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The
stream JSON is auto-synced from the live model whenever the
structured fields change.
State source of truth is a deeply-reactive Inbound + DBInbound pair
cloned on open; submit serializes via inbound.settings.toString() +
inbound.stream.toString() so the wire shape matches the legacy panel
byte-for-byte. streamNetworkChange semantics (clear flow when
TLS/Reality unavailable, reset finalmask.udp when not KCP) are
preserved.
Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full
TLS cert/ECH editor will land in 5f-iii-c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring
Wires up the last batch of inbound row + general actions that were
toasting "coming soon": export-inbound-links, export-subs (per-inbound
and global), export-all-links, import-inbound, and the clipboard JSON
peek. Two small shared components back them — both can be reused by
the xray page later.
- TextModal.vue (shared): read-only multi-line viewer with a copy
button and an optional download button when fileName is set.
Replaces the legacy txtModal which the inbounds page used for every
link export.
- PromptModal.vue (shared): generic title + input/textarea + confirm
callback, with the legacy keybindings (Enter submits in single-line
mode; Ctrl+S submits in textarea mode). Used here for import-inbound
but also by xray-config edits in Phase 6.
- InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs`
on the general-actions menu and `export`/`subs`/`clipboard` on the
per-row menu, routing each through openText / openPrompt + the
appropriate model helper (genInboundLinks, etc.). The copyClients
cross-inbound modal stays toast-stubbed — that's its own dedicated
legacy modal worth its own commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab
The fifth and last legacy page comes online. Tabs are scaffolded with
a-empty placeholders for the structured editors (Basics / Routing /
Outbounds / Balancers / DNS) so navigation is stable; the
Advanced (JSON) tab is fully functional and lets power users edit
the raw xraySetting tree exactly like the legacy CodeMirror pane.
- xray.html + src/xray.js: fifth Vite multi-page entry, mounted as
XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it
through the dev proxy bypass alongside the other pages.
- XrayPage.vue: page chrome with the Save / Restart-xray bar, restart-
output popover (surfaces /panel/xray/getXrayResult content when
startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor.
CodeMirror is intentionally not pulled in — the textarea works for
every modern browser and keeps the bundle slim while structured
editors land in 6-ii through 6-v.
- useXraySetting.js composable: POST /panel/xray/ on mount, mirrors
the settings-page 1s busy-loop dirty check for both xraySetting
and outboundTestUrl, and exposes saveAll + restartXray. The dirty
flag relies on string equality of the pretty-printed JSON, so
reformat-only edits don't enable Save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-ii — xray Basics tab structured editor
Replaces the placeholder on the Basics tab with a structured form for
the most-touched fields of the xray template — outbound + routing
strategy, log levels, traffic stat counters, and the "basic routing"
shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4
forced, WARP / NordVPN routing).
- useXraySetting.js: hoists a parsed `templateSettings` reactive
alongside the JSON string, with two cooperating watches that keep
them in sync. Editing structured fields stringifies into xraySetting
for the dirty-poll + Advanced JSON tab; editing the JSON re-parses
into templateSettings only when valid, so structured tabs stay
readable mid-edit.
- BasicsTab.vue: collapse panels mirror the legacy partial — General,
Statistics, Logs, Basic routing. Every input is a computed v-model
reading/writing into templateSettings; the routing-rule shortcuts
funnel through ruleGetter/ruleSetter which match the legacy
templateRuleGetter/templateRuleSetter behavior (replace-first,
drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters
also call syncOutbound() to provision/prune the matching outbound.
- XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist`
from the parsed templateSettings. WARP/NordVPN provisioning modals
are still placeholders that toast — those land in 6-v with the
routing/outbound editors.
Default tab flips back to Basics so users land on the structured
editor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iii — xray Routing tab + rule modal
Replaces the Routing tab placeholder with a full editor for
templateSettings.routing.rules:
- RoutingTab.vue: a-table over the parsed rules with the legacy six-
column layout (action / source / network / destination / inbound /
outbound) and the same "lead value + N more" pill renderer for
multi-value criteria. Mobile drops source/network/destination for
readability. Per-row dropdown handles edit / move-up / move-down /
delete; the array-mutation reordering replaces the legacy jQuery
Sortable drag handle without pulling in a sortable lib.
- RuleFormModal.vue: full form mirroring xray_rule_modal.html —
CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port,
Network select, Protocol multi-select, Attrs key/value pairs,
inbound-tag multi-select sourced from
templateSettings.inbounds + parent inboundTags + dnsTag,
outbound-tag single-select sourced from templateSettings.outbounds
+ clientReverseTags, and balancerTag from
templateSettings.routing.balancers. Submit serializes via the
same shape the legacy `getResult` produces (CSV → array, drop
empty fields).
- XrayPage.vue: imports RoutingTab and exposes inboundTags +
clientReverseTags from useXraySetting so the modal can populate
its tag pools.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal
Replaces the Outbounds tab placeholder with a full table + add/edit
flow. The 1.3k-line legacy outbound modal is condensed to a tabbed
modal with structured Basics fields (tag/protocol/sendThrough/domain
strategy) and JSON tabs for the protocol-specific settings + stream
trees — same approach the Inbound modal uses, and a power user can
still edit the same trees via the page-level Advanced (JSON) tab.
- useXraySetting.js: adds fetchOutboundsTraffic +
resetOutboundsTraffic + testOutbound. Test states are tracked per
outbound index so the row's Test button can show loading + the
Test-result column can render the response delay / status / error.
- OutboundsTab.vue: full table (action / identity / address / traffic
/ test result / test) plus a card-list mobile variant with the
same row dropdown (set-first / edit / move up/down / reset traffic
/ delete). outboundAddresses() reproduces the legacy
findOutboundAddress logic so each protocol's host:port list is
rendered consistently. Add/edit go through OutboundFormModal,
delete goes through Modal.confirm, reset traffic posts to
/panel/xray/resetOutboundsTraffic with the row's tag (or
"-alltags-" from the toolbar).
- OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on
the Basics tab; settings + streamSettings as raw JSON on their
respective tabs. Tag-collision check happens client-side before
emitting; malformed JSON aborts the save with a message.error.
- XrayPage.vue: imports OutboundsTab and wires the test action to
the composable's testOutbound helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder
Brings Balancers to full parity with the legacy panel and adds a
DNS tab placeholder that exposes the full dns/fakedns trees as JSON
so users can edit them without falling through to Advanced.
- BalancerFormModal.vue: tag (with duplicate-tag warning across
other balancers), strategy (random/roundRobin/leastLoad/leastPing),
selector tag-mode multi-select sourced from existing outbound
tags + free-form additions, fallback. Disable-on-invalid is
driven by the duplicateTag + emptySelector computed flags.
- BalancersTab.vue: empty state with a single "Add balancer" CTA;
populated state shows the legacy 4-column table (action / tag /
strategy / selector / fallback) with per-row edit + delete in a
dropdown. On submit the wire shape preserves the
`strategy: { type }` nesting only when the strategy is non-default,
matching the legacy emit. Tag renames also chase across
routing.rules.balancerTag references so existing rules don't dangle.
- DnsTab.vue: master enable switch + raw JSON for `dns` and
`fakedns`. Legacy had a dedicated server-by-server editor + a
fakedns row editor; both are big enough to deserve their own
commits, and the JSON path supports every field today.
WARP / NordVPN provisioning modals still toast as "coming soon" —
those are third-party API integrations worth their own commits.
The xray page now has structured editors for Basics / Routing /
Outbounds / Balancers and JSON editors for DNS / Advanced — every
xray tab the legacy panel offered is functional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(server): Phase 8 — cut HTML routes over to web/dist/
Production cutover. Every user-facing HTML route now serves the
Vue-3-built bundle from web/dist/ instead of rendering the legacy
Go template; the long-hashed Vite assets are served at /assets/ from
the same embedded filesystem. The legacy templates in web/html/ and
the legacy static tree in web/assets/ are kept on disk for now in
case a quick revert is needed, but nothing the binary serves
references them.
What changed:
- web.go: a new //go:embed dist/* feeds the controller package via
a SetDistFS hand-off before controller construction. The static
/assets/ route is rebound: in dev to web/dist/assets/ on disk so
Vite's incremental rebuilds show up live; in prod to the embedded
dist via wrapDistFS (rooted one level deeper than wrapAssetsFS).
- controller/dist.go: serveDistPage helper used by every HTML
handler. Reads dist/<name> from the embedded FS and applies two
transforms before sending:
1. injects <script>window.__X_UI_BASE_PATH__="..."</script>
just before </head> so AppSidebar links resolve under the
panel's basePath.
2. when basePath != "/", rewrites Vite's absolute /assets/ URLs
to <basePath>assets/ so installs running under a custom URL
prefix load the bundle where the static handler lives.
HTML responses go out with no-cache so panel upgrades reach
users on the next refresh; hashed JS/CSS stays cacheable.
- controller/index.go: IndexController.index now serves
dist/login.html for logged-out callers (the redirect for logged-in
users is unchanged).
- controller/xui.go: XUIController.{index,inbounds,settings,xraySettings}
each become a one-line wrapper around serveDistPage.
Smoke checklist for the maintainer:
- run `cd frontend && npm run build` to refresh web/dist/ before
building the Go binary (the embed snapshot is taken at compile
time);
- visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and
confirm each loads its Vue page;
- log out and log back in to verify the login flow;
- confirm the sidebar links navigate correctly under your install's
basePath;
- POST flows (e.g. saving settings) still need the CSRF token —
that endpoint (/panel/csrf-token, added earlier) is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals
Replaces the toast stubs on the Basics tab and Outbounds toolbar
with the legacy WARP + NordVPN provisioning flows. Both modals now
stage their wireguard outbounds back into templateSettings.outbounds
through the same event channels OutboundsTab uses, so the existing
add / reset / delete / refresh-traffic surface keeps working.
- WarpModal.vue: empty state shows a single Create button that
generates a wireguard keypair locally (Wireguard.generateKeypair)
and posts it to /panel/xray/warp/reg; populated state surfaces
the access_token / device_id / license_key / private_key, lets
the user upgrade to WARP+ via /panel/xray/warp/license, refreshes
the account info from /panel/xray/warp/config (plan / quota /
usage in human-readable bytes), and stages a wireguard outbound
with the WARP-specific reserved-byte encoding pulled from
client_id. Add / Reset / Delete go through events the parent
routes back to templateSettings.outbounds.
- NordModal.vue: dual-tab login (NordVPN access token →
/panel/xray/nord/reg, or paste a NordLynx private key →
/panel/xray/nord/setKey). Once authenticated, country / city /
server selectors fetch from /panel/xray/nord/{countries,servers},
servers sort by load ascending, the lowest-load server in the
current city auto-selects. Reset emits oldTag/newTag so the
parent renames matching routing rules in place; logout emits a
remove-routing-rules event with prefix `nord-` to purge any
dangling references.
- XrayPage.vue: holds warpOpen / nordOpen flags, ensures the
outbounds array exists before mutating it, and wires the modal
events (add-outbound / reset-outbound / remove-outbound /
remove-routing-rules) to in-place edits of templateSettings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): Phase 7 — vue-i18n wired up + login page translated
Sets up vue-i18n on top of the panel's existing TOML translation
files. The Go side stays the source of truth — translators continue
to edit web/translation/*.toml; a sync script snapshots those files
into per-locale JSON the Vue bundle imports. The login page is
translated end-to-end as a worked example; remaining pages can be
converted incrementally without infrastructure churn.
What's in the box:
- scripts/sync-locales.mjs: small TOML→JSON converter that walks
web/translation/*.toml and writes frontend/src/locales/<code>.json.
Handles the narrow subset of TOML the panel uses (flat key/value
pairs + dotted [section.subsection] heads). Wired as a `prebuild`
+ `predev` script so production builds always include the latest
strings without a manual step.
- src/i18n/index.js: createI18n() in composition mode with all 13
locales emitted as their own Vite chunks. The active locale (read
from the same `lang` cookie LanguageManager has always managed)
plus the en-US fallback are eagerly loaded; the rest are
dynamically importable via a loadLocale(code) helper. This keeps
the per-page bundle the user actually downloads small — only ~30
KB of strings end up in the initial payload, vs ~220 KB if all
13 were eager.
- All five page entries (index/login/settings/inbounds/xray) wire
the i18n plugin into createApp via .use(i18n).
- LoginPage.vue: t(...) replaces hardcoded English on the username
/ password / 2FA placeholders, the submit button label, and the
Settings popover title. The Hello/Welcome headline cycle stays
hardcoded — those are stylistic, not labels.
The 'Hello'/'Welcome' cycle stays in English deliberately; the rest
of the migration's components still ship hardcoded English and will
be converted page by page in follow-up commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
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>
* i18n(frontend): translate every remaining English string on the index page
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>
* i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs
Continues the page-by-page translation pass started in cb37dd55 — runs
every user-visible string on settings (General/Security/Telegram/Sub),
inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/
Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync
script to escape `@` (vue-i18n parses it as a linked-format prefix) and
refreshes all 13 locale files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles
- 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>
* feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles
(tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback
strategy, client subnet), the servers table with per-row strategy and
domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new
DnsServerModal covers the full add/edit flow and collapses to a bare
string when the user only sets an address — matching the wire shape
the legacy form emits for plain DNS entries like "8.8.8.8".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): rebuild xray outbound modal with structured per-protocol forms
Replaces the JSON textareas with the same shape the legacy panel uses:
all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/
mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated
fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its
own panel, and TLS/Reality/sockopt/Mux are configured through the same
controls as the inbound side. Brings the SPA outbound editor to parity
with main so users no longer have to drop into raw JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): bring inbound modal to full parity with main branch
Switches the default protocol on add to VLESS, fixes a crash when adding
a Mixed account (the constructor is SocksAccount, not MixedAccount),
and fills in the fields the SPA was previously delegating to the
Advanced JSON tab:
- TLS: cipher suites, min/max version, reject SNI / disable system root /
session resumption switches, the certificate array with per-row
Path-or-Content toggle (Set Default pulls from /panel/setting/
defaultSettings), One Time Loading, Usage / Build Chain, plus ECH
key/config with a Get New ECH Cert button.
- Reality: xver, target/SNI sync icons (uses getRandomRealityTarget),
max time diff, min/max client version, short IDs randomizer, SpiderX,
mldsa65 seed/verify with Get New Seed.
- Stream: full structured forms for every transport — TCP HTTP
camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/
downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) /
HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the
full SplitHTTPConfig surface (mode-aware fields, padding obfs,
session/sequence placement, uplink data, no-SSE).
- New External Proxy section and a structured Sockopt block (mark,
TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only,
domain strategy, congestion, TProxy, dialer/interface, trusted XFF).
- VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh
decryption/encryption blocks via /panel/api/server/getNewVlessEnc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound
Mirrors web/html/form/stream/stream_finalmask.html as a shared
FinalMaskForm component used by both modals — they share the same
StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams)
so a single template handles both. Surfaces:
- TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment,
sudoku, and header-custom (with the 2D clients/servers groups, each
row supporting array/str/hex/base64 packets and a randomize button
for base64).
- UDP masks for hysteria protocol or kcp network: hysteria gets just
salamander; kcp gets the full type list (mkcp variants, header-*,
xdns/xicmp, header-custom with flat client/server lists, and noise).
Switching to xdns shrinks the kcp MTU to 900 to match the legacy
panel's behavior.
- QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down
fields), debug, UDP hop ports/interval, idle/keepalive timeouts,
path-MTU discovery toggle, and the four receive-window tunables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): remove duplicate Outbound test URL from xray Advanced tab
The Basics tab already exposes this field through BasicsTab —
duplicating it on the Advanced tab let two inputs race the same
ref and only added clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark
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>
* fix(frontend): restore computed import in Settings + Xray pages
When 5f1aba28 dropped the local antdThemeConfig computed (now shared
from useTheme), it also stripped `computed` from the import list — but
both pages still call computed() elsewhere (confAlerts, advanced-tab
helpers). Re-adds it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them
- 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>
* i18n: shorten backupTitle to "Backup & Restore" across all 13 locales
The backup modal header was the second-longest title in the dashboard
on every locale ("Database Backup & Restore" / "Резервне копіювання
та відновлення бази даних" / etc). Drop the "Database / Veritabanı /
数据库" qualifier — the modal already lives under the "Database"
column, so the shorter form reads cleaner on narrow viewports.
Updated both the .toml source-of-truth files and the synced .json
locales (re-running scripts/sync-locales.mjs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: collapse two translation databases into a single web/translation/<lang>.json set
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>
* fix(frontend): redesign expand-row + retheme client visuals
When you expanded an inbound row, the nested <a-table> inside
ClientRowTable burst out of the parent's scroll-x box — its
.ant-spin-container ended up wider than the parent's narrow
.ant-table-cell, so the child looked oversized while the parent looked
squeezed. Replace the nested table with a CSS-grid layout that owns
its sizing, sits flush inside the expanded cell, and collapses to a
3-column layout on mobile (action menu, client identity, info popover).
While in there, fix three other client-row visuals:
- The Unicode infinity glyph (U+221E) renders as an "m"-shaped
character in some system fonts (Windows Segoe UI in particular).
Add a shared <InfinityIcon /> SVG component (legacy panel's path)
and use it in ClientRowTable, InboundList, and InboundInfoModal —
desktop and mobile cells.
- The "unlimited quota" traffic bar passed :percent="100" with no
stroke-color, so AD-Vue auto-coloured it success-green. Pin it to
the AD-Vue purple token (#722ed1) so it reads as the no-limit
sentinel rather than another usage state.
- ColorUtils + the in-row statsExpColor still hardcoded the legacy
teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c /
#7a316f). Map them onto AD-Vue 4's success/warning/danger/purple
tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags,
and progress bars all match the rest of the panel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): darken light-theme page bg so cards stand out
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>
* fix(frontend): shrink dashboard percent text and surface the unfinished arc
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>
* feat(frontend): migrate subpage.html to Vue 3 SPA
The subscription info page was the last page still rendered by Go
templates. Move it to the Vite multi-page setup so the whole panel
loads through one toolchain.
Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__
for the parsed view-model (traffic / quota / expiry + rendered share
links). Fix descriptions borders against the light-theme card by
painting the row divider on each cell's bottom edge — AD-Vue's <tr>
border doesn't render reliably under border-collapse:collapse.
Backend: serveSubPage reads dist/subpage.html, injects
window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>,
and rewrites Vite's absolute /assets/ URLs when the panel runs under
a URL prefix. Drop the legacy template-FuncMap wiring and switch the
sub server's static mount from web/assets to web/dist/assets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): inbound modal QR + tabs + restored TLS fallbacks
Per-client QR action: the qr icon on the expand-row table opened the
big info modal instead of the QR modal. Route it to QrCodeModal and
extend that modal with a `client` prop so genAllLinks() produces the
per-client share URLs (and per-peer remarks for WireGuard).
Inbound's Data redesign: split the dense single-page view into three
tabs — Inbound, Client, Subscription. Drop every QR rendering from
this modal (QrCodeModal is the QR home now). Each row in the Inbound
tab is one label/value pair instead of the legacy 2-column grid, and
long values like the VLESS encryption blob render as a wrapping code
block with a copy button so they can't blow out the dialog. The
Subscription tab renders sub URL + JSON URL as clickable anchors that
open in a new tab.
Restored TLS fallbacks UI: the model already exposed
VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback /
delFallback / fallbackToJson, but the form modal never surfaced them
during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path,
Destination, PROXY) on the protocol tab, gated on TCP transport plus
(for VLESS) encryption=none — same conditions as main.
Column widths: Protocol 70→130 and All-time Traffic 60→95 in the
inbound list; All-time Traffic 90→130 in the client expand-row, so
the header text fits and tags don't get squeezed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards
#142340, sider #0d1d33) so the sidebar blends with the rest of the
surface; ultra-dark stays neutral black. Resolves the previous mismatch
where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and
dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's
colorItemBg — overrides go through the component-token map now.
Round the inbound table's outer corners (header start/end + last row
end) and wrap the client expand-row grid in a 1px / 8px-radius border
so the list reads as a contained block instead of a flush rectangle.
Linter-driven whitespace cleanup across inbounds/*.vue rolled into the
same commit since it can't be split out cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default
Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used
arrow expressions that returned splice's removed-items array. AD-Vue 4
treats truthy non-thenables from onOk as "still pending" and never closes
the dialog (see ActionButton.js:103-106), so the confirm modal stayed
open. Wrap the body so onOk returns undefined and AD-Vue auto-closes.
Tag validation: outbound + balancer modals only flipped between
warning/success on duplicate, leaving the empty case as a green ✓.
Split into a 3-state computed — error (empty) / warning (duplicate) /
success — and wire a help message so the input clearly explains why
the OK button is disabled.
Reset to default: re-add the legacy "Reset to Default" panel at the
bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and
overwrites templateSettings; the existing watch re-stringifies so the
JSON tab + dirty-poll see the new state.
Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/
Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex
entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex),
ServicesOptions (Reddit/Speedtest in, off-template Microsoft out).
Outbound form parity with main:
• Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes
(HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains
excluded multi-selects, gated on reverseTag being set.
• Full XHTTP transport — request headers list, Max Upload Size /
Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields,
Uplink HTTP Method, Session/Sequence/UplinkData placement +
keys, No gRPC Header (stream-up/stream-one), expanded XMUX with
Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive.
Strip a-divider from the outbound form per request — replaced with
plain section/item heading divs so the labels and per-row delete
icons stay but the horizontal rule is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): xray Advanced tab parity + finalmask gating
Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.
Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.
Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only
refreshed advancedJson.stream, so toggling the Sniffing switch in the
Sniffing tab left the Advanced JSON showing the prior value. And
encryption — stored on inbound.settings.encryption, not on stream —
never appeared at all because Advanced only exposed stream + sniffing.
Split the watch into three (stream / sniffing / settings) and add a
settings textarea so encryption / clients / fallbacks live alongside
the existing two views. The submit() path now reads settings from
the JSON tab too (falling back to inbound.settings.toString()) so
power-user edits in Advanced override the structured form on save.
QR canvas: when a longer share-URL bumps the QR matrix size, QRious
falls back to floor(canvasSize / matrixWidth) and centers the pattern,
leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick
the QR version from the URL byte length and set canvas size to a
multiple of matrixWidth × pixelSize so the pattern always fills it
edge-to-edge — no white margin even after toggling encryption on.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound stream tidy-up + QR sizing + dev proxy
Stream tab clean-up: drop the seven a-divider rules in the inbound
form's Stream tab — replace the labelled ones (Request / Response /
Security) with a section-heading div that matches the outbound modal,
delete the empty rules above TLS sub-blocks / External Proxy /
Sockopt. Empty header-list form-items also leaked margin space below
each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate
each on headers.length > 0 so they vanish until the user adds one.
QR panel: drop the link text under the canvas (the user already has
a copy button on the header). Pin the canvas display size to a fixed
240px square via :style + image-rendering: pixelated/crisp-edges so
a dense WireGuard config QR and its sparser link share the same
on-screen footprint without blurring.
Dev proxy: Node's AggregateError wraps connection failures whenever
DNS returns more than one address (::1 + 127.0.0.1) and the code
lands on the inner errors, not the outer. The existing handler only
checked err.code so the ECONNREFUSED stack still spammed the log
when the Go backend was down. Walk err.errors too, print one
friendly line ("backend not reachable — start the Go server"), then
stay quiet for the rest of the session.
Vendor splitting + chunk-size warning: split node_modules into
stable vendor-* chunks so each page only ships the deps it uses and
the browser caches them across versions. ant-design-vue stays as a
single chunk because its components share internals; raise the
chunk-size warning to 1500kB so the build stays quiet (its 1.4MB
minified gzips to ~410kB on the wire).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): info-modal cleanup + 2FA QR + outbound link import
- 2FA QR: matrix-snap canvas + opaque background to drop white margin
- Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab
strip when only the Inbound tab applies
- Add inline VLESS Reverse tag input on first-client form
- Hide Protocol tab for TUN (no form yet)
- Outbound link converter: route through Outbound.fromLink so
vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray
implicit global in fromLink
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(frontend): jalali calendar + drop legacy moment-jalali
- Wire Calendar Type setting to a real Jalali datepicker via
vue3-persian-datetime-picker, gated by useDatepicker composable
- DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps
dayjs v-model contract so existing forms/setters work unchanged
- Theme picker popup explicitly per body.dark / data-theme=ultra-dark
(AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to
white); fix invisible disabled days, SVG arrow fills, popup clipping
via append-to="body"
- Replace stray moment() calls in dbinbound/inbound models with dayjs;
the legacy global was undefined under ESM and broke the inbounds list
whenever any inbound had expiryTime > 0
- Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker
assets — replaced by the Vue 3 picker
Note: dark/ultra background of the date popup still renders white in
some cases — pending follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): jalali popup theming + full-month layout
- Re-prefix popup selectors with .vpd-wrapper (popup root that travels
with appendTo='body'), not .vpd-main (which stays at the input);
paints the popup's dark/ultra background again
- Drop the 1px border on .vpd-content — with box-sizing: border-box
it ate 2px from the day-row width, wrapping the 7th cell of every
row and hiding days 18-31 of months that needed a 5th week
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: render dates in Jalali when Calendar Type is jalalian
- 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>
* feat(frontend): ultra-dark page tint + mobile-friendly inbound view
- 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>
* chore: remove legacy template + asset trees and dead Go template engine
- Delete web/html/ entirely (page templates, form/, modals/, component/,
common/, settings/) — every route is served from web/dist/ now via
serveDistPage; nothing in the binary referenced these
- Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment,
codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font);
Vite bundles all of this into web/dist/assets
- Drop the Gin HTML template wiring: remove //go:embed assets +
//go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter,
EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate,
the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod
template-engine branch — only StaticFS for /assets/ is needed now
- Remove dead html()/getContext() helpers and unused imports from
web/controller/util.go (no c.HTML(...) callers remain)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(frontend): inbound expand chevron position + cpu history layout
- 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>
* feat(login): language picker in settings popover + fluid card sizing
- Add language select alongside the theme switch (mirrors SubPage)
- Bind headline to pages.login.hello / pages.login.title so the
"Hello / Welcome" cycle re-translates with the active locale
- Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card
scales smoothly instead of jumping ~33% at each breakpoint
- Pin horizontal padding so input width stays stable on large viewports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): organize entry HTML + bootstrap JS into folders
- Move entry HTML files: frontend/*.html -> frontend/html/*.html
- Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/
- Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html
- Build output now lands at web/dist/html/<page>.html
- serveDistPage and subController updated to read from dist/html/
Cleans up the flat frontend/ root which previously interleaved 6 HTML
files with package.json, README, src/, etc. The src/ root similarly
gets rid of 6 entry .js files mixed in alongside api/, components/,
models/, etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: remove obsolete vue3 phase1 inventory doc
The migration is well past phase 1 — the inventory doc has rotted
and the live state lives in the codebase plus the plan files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(frontend): merge utils/legacy.js into utils/index.js
The barrel was a placeholder for an eventual split that hasn't
happened. Collapsing the two files removes one layer of indirection
and the misleading "legacy" name (the contents are still actively
used by the migrated SPA).
- Move all 930 lines from utils/legacy.js into utils/index.js
- Delete utils/legacy.js
- Update direct import in models/outbound.js to '@/utils'
- Drop a stale legacy.js reference in InboundFormModal comment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(frontend): keep entry HTML files at frontend/ root
The earlier move to frontend/html/ made dev-mode URLs ugly
(http://localhost:5173/html/index.html instead of plain /). The folder
didn't add real value — it just hid 6 files behind a non-conventional
layout. Reverting that piece while keeping src/entries/ (which is a
genuine separation between page bootstrap and the rest of src/).
- HTML files back at frontend/<page>.html
- Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths
- Build output is web/dist/<page>.html again
- web/controller/dist.go and sub/subController.go read from dist/<name>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): bump eslint to 10 + add flat config + clean lint warnings
- 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>
* feat(inbounds): one client identity across multiple inbounds via subId
Lets the operator add the same email under the same subId to several
inbounds. Xray reports traffic per email, so a single client_traffics
row acts as the shared accumulator — no aggregation overhead, quota and
expiry stay consistent.
- Email validation allows duplicates only when subId matches
- AddClientStat upserts via OnConflict DoNothing (idempotent on rerun)
- Stat/IP rows survive client deletion when a sibling inbound still
references the email
- enrichClientStats tops up GORM-preloaded stats with rows whose
inbound_id points at a sibling, so every panel view sees usage
- disableInvalidClients cascades enable=false and syncs the row's
total/expiry into every sibling JSON when the shared identity expires
- DelDepletedClients removes the depleted client from all referencing
inbounds, batched
- Subscription services dedupe traffic by email so shared quota is
counted once
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(frontend): rewrite README for multi-page Vue 3 layout
Reflects the current state — embedded build, per-route HTML entries,
ESLint 10 flat config, src/ layout, and the steps to add a new page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build(frontend): drop deprecated rimraf/glob/inflight transitive deps
vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which
pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks
memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same
runtime API, dropped the legacy build deps) so npm install no longer
warns and the dep tree is 12 packages lighter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node)
- Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth
- Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI
- Runtime abstraction (Local + Remote) so inbound/client mutations target the
inbound's owning node instead of always hitting the local xray
- Inbounds gain optional NodeID; tag-based correlation with remote panel (no
RemoteInboundID column needed)
- NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline
from each enabled+online node and writes them into central DB; 30s reset
grace window prevents post-reset overwrite
- Reset propagation to nodes (best-effort) on client/inbound/all reset paths
- Subscription server uses node.Address for inbounds with NodeID, falling back
to existing host resolution for local inbounds
- Frontend: Nodes page, "Deploy to" select in inbound form, Node column on
inbound list, hostOverride threaded through genAllLinks/QR/Info modals
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stats): system history modal + per-node CPU/Mem trends across all locales
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>
* fix(embed): include underscore-prefixed Vite chunks in dist FS
go:embed silently excludes files whose names start with `_` or `.`,
so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown
emits for @vitejs/plugin-vue was missing from the production binary.
First import at runtime hit a 404 and the SPA failed to mount — blank
page on every page load, no error in the server logs because the
asset 404 was just a static-handler miss.
Switched the directive to `//go:embed all:dist` which keeps the same
root layout but disables the underscore/dot exclusion rule. Dev mode
was unaffected (it serves dist/assets/ from disk, not the embedded FS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: build frontend bundle before Go compile in release.yml + Dockerfile
Phase 8 cut all panel HTML routes over to web/dist/ and embedded the
Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is
.gitignored, so on a fresh CI checkout it doesn't exist — every Go
build since Phase 8 has been failing with "pattern dist: no matching
files found" or producing a binary that 404s on first asset request.
release.yml: add a setup-node@v4 + npm ci + npm run build trio before
the existing go build step in both the Linux matrix job (7 arches)
and the Windows job. npm cache is keyed on frontend/package-lock.json.
Dockerfile: add a node:22-alpine frontend stage that runs npm ci +
npm run build and emits to /src/web/dist (via vite.config.js's outDir).
The golang builder stage then COPY --from=frontend /src/web/dist into
./web/dist before the go build, so embed.FS sees the bundle.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh
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>
* feat(inbounds): hide Node column when no nodes are defined
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
|
|
|
|
"ResetAllTraffics": "重設所有流量",
|
|
|
|
|
|
"SortedTrafficUsageReport": "排序過的流量使用報告"
|
|
|
|
|
|
},
|
|
|
|
|
|
"answers": {
|
|
|
|
|
|
"successfulOperation": "✅ 成功!",
|
|
|
|
|
|
"errorOperation": "❗ 操作錯誤。",
|
|
|
|
|
|
"getInboundsFailed": "❌ 獲取入站資訊失敗。",
|
|
|
|
|
|
"getClientsFailed": "❌ 獲取客戶失敗。",
|
|
|
|
|
|
"canceled": "❌ {{ .Email }}:操作已取消。",
|
|
|
|
|
|
"clientRefreshSuccess": "✅ {{ .Email }}:客戶端重新整理成功。",
|
|
|
|
|
|
"IpRefreshSuccess": "✅ {{ .Email }}:IP 重新整理成功。",
|
|
|
|
|
|
"TGIdRefreshSuccess": "✅ {{ .Email }}:客戶端的 Telegram 使用者重新整理成功。",
|
|
|
|
|
|
"resetTrafficSuccess": "✅ {{ .Email }}:流量已重置成功。",
|
|
|
|
|
|
"setTrafficLimitSuccess": "✅ {{ .Email }}: 流量限制儲存成功。",
|
|
|
|
|
|
"expireResetSuccess": "✅ {{ .Email }}:過期天數已重置成功。",
|
|
|
|
|
|
"resetIpSuccess": "✅ {{ .Email }}:成功儲存 IP 限制數量為 {{ .Count }}。",
|
|
|
|
|
|
"clearIpSuccess": "✅ {{ .Email }}:IP 已成功清除。",
|
|
|
|
|
|
"getIpLog": "✅ {{ .Email }}:獲取 IP 日誌。",
|
|
|
|
|
|
"getUserInfo": "✅ {{ .Email }}:獲取 Telegram 使用者資訊。",
|
|
|
|
|
|
"removedTGUserSuccess": "✅ {{ .Email }}:Telegram 使用者已成功移除。",
|
|
|
|
|
|
"enableSuccess": "✅ {{ .Email }}:已成功啟用。",
|
|
|
|
|
|
"disableSuccess": "✅ {{ .Email }}:已成功禁用。",
|
|
|
|
|
|
"askToAddUserId": "未找到您的配置!\r\n請向管理員詢問,在您的配置中使用您的 Telegram 使用者 ChatID。\r\n\r\n您的使用者 ChatID:<code>{{ .TgUserID }}</code>",
|
|
|
|
|
|
"chooseClient": "為入站 {{ .Inbound }} 選擇一個客戶",
|
|
|
|
|
|
"chooseInbound": "選擇一個入站"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.
Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.
Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.
Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 16:03:07 +00:00
|
|
|
|
}
|