3x-ui/web/translation/zh-TW.json

959 lines
45 KiB
JSON
Raw Permalink Normal View History

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": "建立",
"update": "更新",
"copy": "複製",
"copied": "已複製",
"download": "下載",
"remark": "備註",
"enable": "啟用",
"protocol": "協議",
"search": "搜尋",
"filter": "篩選",
"loading": "載入中...",
"second": "秒",
"minute": "分鐘",
"hour": "小時",
"day": "天",
"check": "檢視",
"indefinite": "無限期",
"unlimited": "無限制",
"none": "無",
"qrCode": "二維碼",
"info": "更多資訊",
"edit": "編輯",
"delete": "刪除",
"reset": "重置",
"noData": "無數據。",
"copySuccess": "複製成功",
"sure": "確定",
"encryption": "加密",
"useIPv4ForHost": "使用 IPv4 連接主機",
"transmission": "傳輸",
"host": "主機",
"path": "路徑",
"camouflage": "偽裝",
"status": "狀態",
"enabled": "開啟",
"disabled": "關閉",
"depleted": "耗盡",
"depletingSoon": "即將耗盡",
"offline": "離線",
"online": "線上",
"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": "系統狀態",
"inbounds": "入站列表",
"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 設定",
"apiDocs": "API Docs",
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": "退出登入",
"link": "管理"
},
"pages": {
"login": {
"hello": "你好",
"title": "歡迎",
"loginAgain": "登入時效已過,請重新登入",
"toasts": {
"invalidFormData": "資料格式錯誤",
"emptyUsername": "請輸入使用者名稱",
"emptyPassword": "請輸入密碼",
"wrongUsernameOrPassword": "用戶名、密碼或雙重驗證碼無效。",
"successLogin": "您已成功登入您的帳戶。"
}
},
"index": {
"title": "系統狀態",
"cpu": "CPU",
"logicalProcessors": "邏輯處理器",
"frequency": "頻率",
"swap": "交換空間",
"storage": "儲存",
"memory": "記憶體",
"threads": "執行緒",
"xrayStatus": "Xray",
"stopXray": "停止",
"restartXray": "重啟",
"xraySwitch": "版本",
"xraySwitchClick": "選擇你要切換到的版本",
"xraySwitchClickDesk": "請謹慎選擇,因為較舊版本可能與當前配置不相容",
"xrayUpdates": "Xray 更新",
"updatePanel": "更新面板",
"panelUpdateDesc": "這將把 3X-UI 更新到最新版本並重新啟動面板服務。",
"currentPanelVersion": "目前面板版本",
"latestPanelVersion": "最新面板版本",
"panelUpToDate": "面板已是最新",
"upToDate": "已是最新",
"xrayStatusUnknown": "未知",
"xrayStatusRunning": "運行中",
"xrayStatusStop": "停止",
"xrayStatusError": "錯誤",
"xrayErrorPopoverTitle": "執行Xray時發生錯誤",
"operationHours": "系統正常執行時間",
"systemHistoryTitle": "系統歷史",
"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": "地理檔案更新成功",
"dontRefresh": "安裝中,請勿重新整理此頁面",
"logs": "日誌",
"config": "配置",
"backup": "備份和恢復",
"backupTitle": "備份和恢復",
"exportDatabase": "備份",
"exportDatabaseDesc": "點擊下載包含當前資料庫備份的 .db 文件到您的設備。",
"importDatabase": "恢復",
"importDatabaseDesc": "點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。",
"importDatabaseSuccess": "資料庫匯入成功",
"importDatabaseError": "匯入資料庫時發生錯誤",
"readDatabaseError": "讀取資料庫時發生錯誤",
"getDatabaseError": "檢索資料庫時發生錯誤",
"getConfigError": "檢索設定檔時發生錯誤",
"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 來源更新失敗",
"customGeoEmpty": "尚無自訂 geo 來源 — 點擊「新增」以建立"
},
"inbounds": {
"allTimeTraffic": "累計總流量",
"allTimeTrafficUsage": "所有时间总使用量",
"title": "入站列表",
"totalDownUp": "總上傳 / 下載",
"totalUsage": "總用量",
"inboundCount": "入站數量",
"operate": "選單",
"enable": "啟用",
"remark": "備註",
"node": "節點",
"deployTo": "部署到",
"localPanel": "本機面板",
"protocol": "協議",
"port": "埠",
"portMap": "埠映射",
"traffic": "流量",
"details": "詳細資訊",
"transportConfig": "傳輸配置",
"expireDate": "到期時間",
"createdAt": "建立時間",
"updatedAt": "更新時間",
"resetTraffic": "重置流量",
"addInbound": "新增入站",
"generalActions": "通用操作",
"modifyInbound": "修改入站",
"deleteInbound": "刪除入站",
"deleteInboundContent": "確定要刪除入站嗎?",
"deleteClient": "刪除客戶端",
"deleteClientContent": "確定要刪除客戶端嗎?",
"resetTrafficContent": "確定要重置流量嗎?",
"copyLink": "複製連結",
"address": "地址",
"network": "網路",
"destinationPort": "目標埠",
"targetAddress": "目標地址",
"monitorDesc": "留空表示監聽所有 IP",
"meansNoLimit": "= 無限制單位GB)",
"totalFlow": "總流量",
"leaveBlankToNeverExpire": "留空表示永不過期",
"noRecommendKeepDefault": "建議保留預設值",
"certificatePath": "檔案路徑",
"certificateContent": "檔案內容",
"publicKey": "公鑰",
"privatekey": "私鑰",
"clickOnQRcode": "點選二維碼複製",
"client": "客戶",
"export": "匯出連結",
"clone": "複製",
"cloneInbound": "複製",
"cloneInboundContent": "此入站規則除埠Port、監聽 IPListening 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": "從面板設定證書",
"telegramDesc": "請提供Telegram聊天ID。在機器人中使用'/id'命令)或({'@'}userinfobot",
"subscriptionDesc": "要找到你的訂閱 URL請導航到“詳細資訊”。此外你可以為多個客戶端使用相同的名稱。",
"info": "資訊",
"same": "相同",
"inboundData": "入站資料",
"exportInbound": "匯出入站規則",
"import": "匯入",
"importInbound": "匯入入站規則",
"periodicTrafficResetTitle": "流量重置",
"periodicTrafficResetDesc": "按指定間隔自動重置流量計數器",
"lastReset": "上次重置",
"periodicTrafficReset": {
"never": "從不",
"daily": "每日",
"weekly": "每週",
"monthly": "每月",
"hourly": "每小時"
},
"toasts": {
"obtain": "獲取",
"updateSuccess": "更新成功",
"logCleanSuccess": "日誌已清除",
"inboundsUpdateSuccess": "入站連接已成功更新",
"inboundUpdateSuccess": "入站連接已成功更新",
"inboundCreateSuccess": "入站連接已成功建立",
"inboundDeleteSuccess": "入站連接已成功刪除",
"inboundClientAddSuccess": "已新增入站客戶端",
"inboundClientDeleteSuccess": "入站客戶端已刪除",
"inboundClientUpdateSuccess": "入站客戶端已更新",
"delDepletedClientsSuccess": "所有耗盡客戶端已刪除",
"resetAllClientTrafficSuccess": "客戶端所有流量已重置",
"resetAllTrafficSuccess": "所有流量已重置",
"resetInboundClientTrafficSuccess": "流量已重置",
"trafficGetError": "取得流量資料時發生錯誤",
"getNewX25519CertError": "取得X25519憑證時發生錯誤。",
"getNewmldsa65Error": "取得mldsa65憑證時發生錯誤。",
"getNewVlessEncError": "取得VlessEnc憑證時發生錯誤。"
},
"stream": {
"general": {
"request": "請求",
"response": "響應",
"name": "名稱",
"value": "值"
},
"tcp": {
"version": "版本",
"method": "方法",
"path": "路徑",
"status": "狀態",
"statusDescription": "狀態說明",
"requestHeader": "請求頭",
"responseHeader": "響應頭"
}
}
},
"client": {
"add": "新增客戶端",
"edit": "編輯客戶端",
"submitAdd": "新增客戶端",
"submitEdit": "儲存修改",
"clientCount": "客戶端數量",
"bulk": "批量建立",
"copyFromInbound": "從入站複製用戶端",
"copyToInbound": "複製用戶端到",
"copySelected": "複製所選",
"copySource": "來源",
"copyEmailPreview": "最終郵箱預覽",
"copySelectSourceFirst": "請先選擇來源入站。",
"copyResult": "複製結果",
"copyResultSuccess": "複製成功",
"copyResultNone": "沒有可複製的內容:未選擇用戶端或來源為空",
"copyResultErrors": "複製錯誤",
"copyFlowLabel": "新用戶端的 Flow (VLESS)",
"copyFlowHint": "套用於所有複製的用戶端。留空則略過。",
"selectAll": "全選",
"clearAll": "全不選",
"method": "方法",
"first": "置頂",
"last": "置底",
"prefix": "字首",
"postfix": "字尾",
"delayedStart": "首次使用後開始",
"expireDays": "期間",
"days": "天",
"renew": "自動續訂",
"renewDesc": "到期後自動續訂。(0 = 禁用)(單位: 天)"
},
"nodes": {
"title": "節點",
"addNode": "新增節點",
"editNode": "編輯節點",
"totalNodes": "節點總數",
"onlineNodes": "線上",
"offlineNodes": "離線",
"avgLatency": "平均延遲",
"name": "名稱",
"namePlaceholder": "例如de-frankfurt-1",
"addressPlaceholder": "panel.example.com 或 1.2.3.4",
"remark": "備註",
"scheme": "協議",
"address": "位址",
"port": "埠",
"basePath": "基礎路徑",
"apiToken": "API 權杖",
"apiTokenPlaceholder": "遠端面板設定頁中的權杖",
"apiTokenHint": "遠端面板在 設定 → API 權杖 中顯示其 API 權杖。",
"regenerate": "重新產生權杖",
"regenerateConfirm": "重新產生會使目前的權杖失效。任何使用該權杖的中央面板將失去存取權,直到更新為止。是否繼續?",
"enable": "已啟用",
"status": "狀態",
"cpu": "CPU",
"mem": "記憶體",
"uptime": "執行時間",
"latency": "延遲",
"lastHeartbeat": "上次心跳",
"xrayVersion": "Xray 版本",
"actions": "操作",
"probe": "立即探測",
"testConnection": "測試連線",
"connectionOk": "連線正常 ({ms} ms)",
"connectionFailed": "連線失敗",
"never": "從未",
"justNow": "剛剛",
"deleteConfirmTitle": "刪除節點「{name}」?",
"deleteConfirmContent": "這將停止監控該節點。遠端面板本身不受影響。",
"statusValues": {
"online": "線上",
"offline": "離線",
"unknown": "未知"
},
"toasts": {
"list": "載入節點失敗",
"obtain": "載入節點失敗",
"add": "新增節點",
"update": "更新節點",
"delete": "刪除節點",
"deleted": "節點已刪除",
"test": "測試連線",
"fillRequired": "名稱、位址、埠與 API 權杖為必填",
"probeFailed": "探測失敗"
}
},
"settings": {
"title": "面板設定",
"save": "儲存",
"infoDesc": "此處的所有更改都需要儲存並重啟面板才能生效",
"restartPanel": "重啟面板",
"restartPanelDesc": "確定要重啟面板嗎?若重啟後無法訪問面板,請前往伺服器檢視面板日誌資訊",
"restartPanelSuccess": "面板已成功重新啟動",
"actions": "操作",
"resetDefaultConfig": "重置為預設配置",
"panelSettings": "常規",
"securitySettings": "安全設定",
"TGBotSettings": "Telegram 機器人配置",
"panelListeningIP": "面板監聽 IP",
"panelListeningIPDesc": "預設留空監聽所有 IP",
"panelListeningDomain": "面板監聽域名",
"panelListeningDomainDesc": "預設情況下留空以監視所有域名和 IP 地址",
"panelPort": "面板監聽埠",
"panelPortDesc": "重啟面板生效",
"publicKeyPath": "面板證書公鑰檔案路徑",
"publicKeyPathDesc": "填寫一個 '/' 開頭的絕對路徑",
"privateKeyPath": "面板證書金鑰檔案路徑",
"privateKeyPathDesc": "填寫一個 '/' 開頭的絕對路徑",
"panelUrlPath": "面板 url 根路徑",
"panelUrlPathDesc": "必須以 '/' 開頭,以 '/' 結尾",
"pageSize": "分頁大小",
"pageSizeDesc": "定義入站表的頁面大小。設定 0 表示禁用",
"remarkModel": "備註模型和分隔符",
"datepicker": "日期選擇器",
"datepickerPlaceholder": "選擇日期",
"datepickerDescription": "選擇器日曆類型指定到期日期",
"sampleRemark": "備註示例",
"oldUsername": "原使用者名稱",
"currentPassword": "原密碼",
"newUsername": "新使用者名稱",
"newPassword": "新密碼",
"telegramBotEnable": "啟用 Telegram 機器人",
"telegramBotEnableDesc": "啟用 Telegram 機器人功能",
"telegramToken": "Telegram 機器人令牌token",
"telegramTokenDesc": "從 '{'@'}BotFather' 獲取的 Telegram 機器人令牌",
"telegramProxy": "SOCKS5 Proxy",
"telegramProxyDesc": "啟用 SOCKS5 代理連線到 Telegram根據指南調整設定",
"telegramAPIServer": "Telegram API Server",
"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 訂閱端點。",
"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": "客戶端應用中將顯示剩餘流量和日期資訊",
"subURI": "反向代理 URI",
"subURIDesc": "用於代理後面的訂閱 URL 的 URI 路徑",
"externalTrafficInformEnable": "外部交通通知",
"externalTrafficInformEnableDesc": "每次流量更新時通知外部 API",
"externalTrafficInformURI": "外部流量通知 URI",
"externalTrafficInformURIDesc": "流量更新將會傳送到此 URI",
"restartXrayOnClientDisable": "用戶自動停用後重新啟動 Xray",
"restartXrayOnClientDisableDesc": "當用戶因到期或流量上限而被自動停用時,重新啟動 Xray。",
"fragment": "分片",
"fragmentDesc": "啟用 TLS hello 資料包分片",
"fragmentSett": "設定",
"noisesDesc": "啟用 Noises.",
"noisesSett": "Noises 設定",
"mux": "多路複用器",
"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": "雙重身份驗證已成功刪除",
"twoFactorModalError": "驗證碼錯誤"
},
"toasts": {
"modifySettings": "參數已更改。",
"getSettings": "取得參數時發生錯誤",
"modifyUserError": "變更管理員憑證時發生錯誤。",
"modifyUser": "您已成功變更管理員憑證。",
"originalUserPassIncorrect": "原使用者名稱或原密碼錯誤",
"userPassMustBeNotEmpty": "新使用者名稱和新密碼不能為空",
"getOutboundTrafficError": "取得出站流量錯誤",
"resetOutboundTrafficError": "重設出站流量錯誤"
}
},
"xray": {
"title": "Xray 配置",
"save": "儲存",
"restart": "重新啟動 Xray",
"restartSuccess": "Xray 已成功重新啟動",
"stopSuccess": "Xray 已成功停止",
"restartError": "重新啟動Xray時發生錯誤。",
"stopError": "停止Xray時發生錯誤。",
"basicTemplate": "基礎配置",
"advancedTemplate": "高階配置",
"generalConfigs": "常規配置",
"generalConfigsDesc": "這些選項將決定常規配置",
"logConfigs": "日誌",
"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 協議",
"Inbounds": "入站規則",
"InboundsDesc": "接受來自特定客戶端的流量",
"Outbounds": "出站規則",
"Balancers": "負載均衡",
"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": "逗號分隔的項目"
},
"outbound": {
"addOutbound": "新增出站",
"addReverse": "新增反向",
"editOutbound": "編輯出站",
"editReverse": "編輯反向",
"reverseTag": "反向標籤",
"reverseTagDesc": "VLESS 簡易反向代理出站標籤。留空則停用。設定後,此客戶端的連線可作為反向代理隧道。",
"reverseTagPlaceholder": "出站標籤(留空則停用)",
"tag": "標籤",
"tagDesc": "唯一標籤",
"address": "地址",
"reverse": "反向",
"domain": "域名",
"type": "類型",
"bridge": "Bridge",
"portal": "Portal",
"link": "連結",
"intercon": "互連",
"settings": "設定",
"accountInfo": "帳戶資訊",
"outboundStatus": "出站狀態",
"sendThrough": "傳送通過",
"test": "測試",
"testResult": "測試結果",
"testing": "正在測試連接...",
"testSuccess": "測試成功",
"testFailed": "測試失敗",
"testError": "測試出站失敗",
"nordvpn": "NordVPN",
"accessToken": "訪問令牌",
"country": "國家",
"server": "伺服器",
"city": "城市",
"allCities": "所有城市",
"privateKey": "私密金鑰",
"load": "負載"
},
"balancer": {
"addBalancer": "新增負載均衡",
"editBalancer": "編輯負載均衡",
"balancerStrategy": "策略",
"balancerSelectors": "選擇器",
"tag": "標籤",
"tagDesc": "唯一標籤",
"balancerDesc": "無法同時使用 balancerTag 和 outboundTag。如果同時使用則只有 outboundTag 會生效。"
},
"wireguard": {
"secretKey": "金鑰",
"publicKey": "公鑰",
"allowedIPs": "允許的 IP",
"endpoint": "端點",
"psk": "共享金鑰",
"domainStrategy": "域策略"
},
"tun": {
"nameDesc": "TUN 介面的名稱。預設值為 'xray0'",
"mtuDesc": "最大傳輸單元。資料包的最大大小。預設值為 1500",
"userLevel": "用戶級別",
"userLevelDesc": "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
},
"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": "編輯伺服器",
"domains": "域",
"expectIPs": "預期 IP",
"unexpectIPs": "意外IP",
"useSystemHosts": "使用系統Hosts",
"useSystemHostsDesc": "使用已安裝系統的hosts檔案",
"usePreset": "使用範本",
"dnsPresetTitle": "DNS範本",
"dnsPresetFamily": "家庭",
"serveStale": "提供過期結果",
"serveStaleDesc": "在背景重新整理時傳回過期的快取結果",
"serveExpiredTTL": "過期TTL",
"serveExpiredTTLDesc": "過期快取項目的有效期0 = 永不過期",
"timeoutMs": "逾時 (毫秒)",
"skipFallback": "跳過回退",
"finalQuery": "最終查詢",
"hosts": "Hosts",
"hostsAdd": "新增 Host",
"hostsEmpty": "未定義任何 Host",
"hostsDomain": "網域 (例如 domain:example.com)",
"hostsValues": "IP 或網域 — 輸入後按 Enter",
"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": "🔴 離線",
"online": "🟢 在線",
"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",
"hostname": "💻 主機名:{{ .Hostname }}\r\n",
"version": "🚀 X-UI 版本:{{ .Version }}\r\n",
"xrayVersion": "📡 Xray 版本: {{ .XrayVersion }}\r\n",
"ipv6": "🌐 IPv6{{ .IPv6 }}\r\n",
"ipv4": "🌐 IPv4{{ .IPv4 }}\r\n",
"ip": "🌐 IP{{ .IP }}\r\n",
"ips": "🔢 IP 地址:\r\n{{ .IPs }}\r\n",
"serverUpTime": "⏳ 伺服器執行時間:{{ .UpTime }} {{ .Unit }}\r\n",
"serverLoad": "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
"serverMemory": "📋 伺服器記憶體:{{ .Current }}/{{ .Total }}\r\n",
"tcpCount": "🔹 TCP 連線數:{{ .Count }}\r\n",
"udpCount": "🔸 UDP 連線數:{{ .Count }}\r\n",
"traffic": "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
"xrayStatus": " Xray 狀態:{{ .State }}\r\n",
"username": "👤 使用者名稱:{{ .Username }}\r\n",
"reason": "❗️ 原因:{{ .Reason }}\r\n",
"time": "⏰ 時間:{{ .Time }}\r\n",
"inbound": "📍 入站:{{ .Remark }}\r\n",
"port": "🔌 埠:{{ .Port }}\r\n",
"expire": "📅 過期日期:{{ .Time }}\r\n",
"expireIn": "📅 剩餘時間:{{ .Time }}\r\n",
"active": "💡 啟用:{{ .Enable }}\r\n",
"enabled": "🚨 已啟用:{{ .Enable }}\r\n",
"online": "🌐 連線狀態:{{ .Status }}\r\n",
"lastOnline": "🔙 上次上線: {{ .Time }}\r\n",
"email": "📧 郵箱:{{ .Email }}\r\n",
"upload": "🔼 上傳↑:{{ .Upload }}\r\n",
"download": "🔽 下載↓:{{ .Download }}\r\n",
"total": "📊 總計:{{ .UpDown }} / {{ .Total }}\r\n",
"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": "✅ 是的",
"no": "❌ 沒有",
"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": "🔘 啟用/禁用",
"custom": "🔢 風俗",
"confirmNumber": "✅ 確認: {{ .Num }}",
"confirmNumberAdd": "✅ 確認新增:{{ .Num }}",
"limitTraffic": "🚧 流量限制",
"getBanLogs": "禁止日誌",
"allClients": "所有客戶",
"addClient": "新增客戶",
"submitDisable": "以停用方式送出 ☑️",
"submitEnable": "以啟用方式送出 ✅",
"use_default": "🏷️ 使用預設值",
"change_id": "⚙️🔑 ID",
"change_password": "⚙️🔑 密碼",
"change_email": "⚙️📧 電子郵件",
"change_comment": "⚙️💬 評論",
"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": "選擇一個入站"
}
}
}